@saasicat/nest 0.19.0 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/dist/_entries.cjs +337 -376
  2. package/dist/admin/index.js +2 -1
  3. package/dist/billing/index.d.cts +7 -6
  4. package/dist/billing/index.d.ts +7 -6
  5. package/dist/billing/index.js +4 -3
  6. package/dist/catalog/index.js +2 -2
  7. package/dist/checkout-offer/index.js +1 -1
  8. package/dist/{chunk-6WA4X5U3.js → chunk-3DRZ5GCP.js} +40 -90
  9. package/dist/{chunk-EUU3B6NB.js → chunk-4O6R6ARQ.js} +12 -6
  10. package/dist/{chunk-AFK2IE2L.js → chunk-55QGAZRQ.js} +1 -1
  11. package/dist/{chunk-F6DCLVBB.js → chunk-AGOG5B77.js} +8 -2
  12. package/dist/chunk-EBJJNTW6.js +22 -0
  13. package/dist/{chunk-7JIHG53V.js → chunk-FYWLPYBK.js} +4 -1
  14. package/dist/{chunk-LXUBCLRK.js → chunk-GC5AKN44.js} +9 -8
  15. package/dist/{chunk-I77VS3YH.js → chunk-NZLAYCEX.js} +16 -22
  16. package/dist/{chunk-QLJHIIOY.js → chunk-VQQWF7MW.js} +7 -3
  17. package/dist/{chunk-ORWIGSNN.js → chunk-XTVAFJ3P.js} +2 -1
  18. package/dist/{chunk-KUDKD7ZG.js → chunk-ZH5PYEMI.js} +19 -21
  19. package/dist/{define-saasicat-CqkhnHn-.d.ts → define-saasicat-B20Ms8UC.d.ts} +1 -1
  20. package/dist/{define-saasicat-D8c6Bs4T.d.cts → define-saasicat-Cf9DpEsY.d.cts} +1 -1
  21. package/dist/{enforce-quota.interceptor-Dp8wTYro.d.ts → enforce-quota.interceptor-CUyfR4cQ.d.ts} +1 -1
  22. package/dist/{enforce-quota.interceptor-Dk0eEtzV.d.cts → enforce-quota.interceptor-Dtg5WdFj.d.cts} +1 -1
  23. package/dist/entitlement/index.js +2 -1
  24. package/dist/index.d.cts +27 -8
  25. package/dist/index.d.ts +27 -8
  26. package/dist/index.js +14 -10
  27. package/dist/{module-BwD-wAor.d.ts → module-BE17XFLg.d.ts} +1 -1
  28. package/dist/{module-DsDkbawm.d.cts → module-CHMIyWlf.d.cts} +1 -1
  29. package/dist/platform/index.d.cts +8 -8
  30. package/dist/platform/index.d.ts +8 -8
  31. package/dist/platform/index.js +9 -8
  32. package/dist/promo/index.d.cts +3 -3
  33. package/dist/promo/index.d.ts +3 -3
  34. package/dist/promo/index.js +3 -2
  35. package/dist/registration/index.js +2 -1
  36. package/dist/{saas-platform.module-DducnQ9s.d.cts → saas-platform.module-BfaYfL-G.d.cts} +1 -1
  37. package/dist/{saas-platform.module-BQmrDGVU.d.ts → saas-platform.module-DChhDrR-.d.ts} +1 -1
  38. package/dist/{service-DPZ2W1E-.d.ts → service-D48clObR.d.cts} +1 -1
  39. package/dist/{service-DPZ2W1E-.d.cts → service-D48clObR.d.ts} +1 -1
  40. package/dist/{tenant-billing.controller-m42YQ_Ei.d.cts → tenant-billing.controller-BIF-mstX.d.cts} +1 -1
  41. package/dist/{tenant-billing.controller-b1G1yB09.d.ts → tenant-billing.controller-DT5UrnDX.d.ts} +1 -1
  42. package/dist/testing/index.d.cts +5 -5
  43. package/dist/testing/index.d.ts +5 -5
  44. package/dist/testing/index.js +9 -8
  45. package/package.json +3 -3
@@ -25,7 +25,7 @@ import {
25
25
  SuperAdminGuard,
26
26
  SuspendTenantDto,
27
27
  buildAdminStatsController
28
- } from "../chunk-7JIHG53V.js";
28
+ } from "../chunk-FYWLPYBK.js";
29
29
  import {
30
30
  AUDIT_PORT_TOKEN,
31
31
  AdminAuditService,
@@ -33,6 +33,7 @@ import {
33
33
  RLS_BYPASS_PORT_TOKEN
34
34
  } from "../chunk-E56W4U2P.js";
35
35
  import "../chunk-YSC46BGM.js";
36
+ import "../chunk-EBJJNTW6.js";
36
37
  import "../chunk-SABTXESR.js";
37
38
  import "../chunk-SHUYVCID.js";
38
39
  export {
@@ -8,10 +8,10 @@ import { c as EntitlementService } from '../aggregation-SKfXu09x.cjs';
8
8
  import { P as ProviderSpec } from '../di-CcNeq9v-.cjs';
9
9
  import { f as ContractFreezePort, g as ContractFreezeSourcePort } from '../subscription-bundles.module-DCpV-Pb5.cjs';
10
10
  export { A as AUDIT_CONTEXT_RESOLVER_TOKEN, a as AuditContextResolver, b as AuthGuardList, C as CONTRACT_FREEZE_PORT_TOKEN, c as CONTRACT_FREEZE_PROJECT_KEY_TOKEN, d as CONTRACT_FREEZE_SOURCE_PORT_TOKEN, e as ContractFreezeBundleSnapshot, D as DuePendingPlanChange, P as PENDING_PLAN_QUERY_PORT_TOKEN, h as PendingPlanQueryPort, S as SELF_SERVICE_BLOCKED_BUNDLES_TOKEN, i as SELF_SERVICE_BLOCKED_PLANS_TOKEN, j as SUBSCRIPTION_USAGE_PORT_TOKEN, k as SUBSCRIPTION_WRITE_PORT_TOKEN, l as SelfServiceBlockedBundles, m as SelfServiceBlockedPlans, n as SubscriptionBundleControllerOptions, o as SubscriptionBundleModule, p as SubscriptionBundleModuleOptions, T as TENANT_AUTH_GUARDS_TOKEN, q as TENANT_ID_RESOLVER_TOKEN, r as TRIAL_PROJECTION_PORT_TOKEN, s as TenantBillingModule, t as TenantBillingModuleOptions, u as TenantIdResolver, v as TrialProjectionInput, w as TrialProjectionPort, U as USAGE_SNAPSHOT_PORT_TOKEN, x as USER_EMAIL_RESOLVER_TOKEN, y as USER_ID_RESOLVER_TOKEN, z as UserEmailResolver, B as UserIdResolver } from '../subscription-bundles.module-DCpV-Pb5.cjs';
11
- export { A as AddBundleToSubscriptionInput, B as BundlePreviewSnapshot, C as CancelBundleFromSubscriptionInput, a as CancelSubscriptionDto, b as ChangePlanDto, c as CompleteOnboardingSubscriptionDto, d as ComposedTenantAuthGuard, L as LimitsCheckRow, P as PendingPlanMaterializationService, e as PlanChangeContext, f as PlanChangePreviewDto, g as PlanChangePreviewIssue, h as PlanChangePreviewService, i as PlanChangeType, j as PlanSnapshotDto, k as PreviewPlanChangeDto, l as ProrationDto, m as ProrationInput, R as RedundantFeatureHint, S as SubscriptionBundleAddPreviewDto, n as SubscriptionBundleCancelPreviewDto, o as SubscriptionBundleConfig, p as SubscriptionBundlePreviewContext, q as SubscriptionBundlePreviewIssue, r as SubscriptionBundlePreviewService, s as SubscriptionBundlesService, T as TenantAdminGuard, t as TenantBillingController, U as UsageResponse, u as addMonths, v as computeProration, w as resolveBundleCancelEffectiveAt, x as resolveBundlePriceNet } from '../tenant-billing.controller-m42YQ_Ei.cjs';
11
+ export { A as AddBundleToSubscriptionInput, B as BundlePreviewSnapshot, C as CancelBundleFromSubscriptionInput, a as CancelSubscriptionDto, b as ChangePlanDto, c as CompleteOnboardingSubscriptionDto, d as ComposedTenantAuthGuard, L as LimitsCheckRow, P as PendingPlanMaterializationService, e as PlanChangeContext, f as PlanChangePreviewDto, g as PlanChangePreviewIssue, h as PlanChangePreviewService, i as PlanChangeType, j as PlanSnapshotDto, k as PreviewPlanChangeDto, l as ProrationDto, m as ProrationInput, R as RedundantFeatureHint, S as SubscriptionBundleAddPreviewDto, n as SubscriptionBundleCancelPreviewDto, o as SubscriptionBundleConfig, p as SubscriptionBundlePreviewContext, q as SubscriptionBundlePreviewIssue, r as SubscriptionBundlePreviewService, s as SubscriptionBundlesService, T as TenantAdminGuard, t as TenantBillingController, U as UsageResponse, u as addMonths, v as computeProration, w as resolveBundleCancelEffectiveAt, x as resolveBundlePriceNet } from '../tenant-billing.controller-BIF-mstX.cjs';
12
12
  import { S as SubscriptionContractService } from '../subscription-contract.service--cm47ZJJ.cjs';
13
13
  import '../plan-resolution-CFCoUkrE.cjs';
14
- import '../service-DPZ2W1E-.cjs';
14
+ import '../service-D48clObR.cjs';
15
15
  import '../admin-audit.service-9IqXMlZm.cjs';
16
16
 
17
17
  /**
@@ -197,10 +197,11 @@ declare class FeatureGuard implements CanActivate {
197
197
  constructor(reflector: Reflector, entitlements: EntitlementService, config?: FeatureGuardConfig | null, upsellResolver?: UpsellOfferResolver | null);
198
198
  canActivate(context: ExecutionContext): Promise<boolean>;
199
199
  /**
200
- * Upsell response (#36): with a registered `UpsellOfferResolver` the 403
201
- * becomes machine-readable (`FeatureNotLicensedBody`), so consumer UIs can
202
- * render a purchase offer. Without a resolver the body carries the same
203
- * `code` but no offers.
200
+ * Upsell response (#36): the 403 body is always the full
201
+ * `FeatureNotLicensedBody`, so a consumer that matches on `code` can read
202
+ * `featureKey`/`offers` unconditionally. Without a registered
203
+ * `UpsellOfferResolver` there is simply nothing to offer — `offers` is
204
+ * empty, the same degradation a failing resolver produces.
204
205
  *
205
206
  * Deliberately 403 + `code` field instead of 402 — rationale in
206
207
  * `@saasicat/types` upsell.types.ts (402 is reserved / inconsistently
@@ -8,10 +8,10 @@ import { c as EntitlementService } from '../aggregation-o0LMenvl.js';
8
8
  import { P as ProviderSpec } from '../di-CcNeq9v-.js';
9
9
  import { f as ContractFreezePort, g as ContractFreezeSourcePort } from '../subscription-bundles.module-BQmoozm5.js';
10
10
  export { A as AUDIT_CONTEXT_RESOLVER_TOKEN, a as AuditContextResolver, b as AuthGuardList, C as CONTRACT_FREEZE_PORT_TOKEN, c as CONTRACT_FREEZE_PROJECT_KEY_TOKEN, d as CONTRACT_FREEZE_SOURCE_PORT_TOKEN, e as ContractFreezeBundleSnapshot, D as DuePendingPlanChange, P as PENDING_PLAN_QUERY_PORT_TOKEN, h as PendingPlanQueryPort, S as SELF_SERVICE_BLOCKED_BUNDLES_TOKEN, i as SELF_SERVICE_BLOCKED_PLANS_TOKEN, j as SUBSCRIPTION_USAGE_PORT_TOKEN, k as SUBSCRIPTION_WRITE_PORT_TOKEN, l as SelfServiceBlockedBundles, m as SelfServiceBlockedPlans, n as SubscriptionBundleControllerOptions, o as SubscriptionBundleModule, p as SubscriptionBundleModuleOptions, T as TENANT_AUTH_GUARDS_TOKEN, q as TENANT_ID_RESOLVER_TOKEN, r as TRIAL_PROJECTION_PORT_TOKEN, s as TenantBillingModule, t as TenantBillingModuleOptions, u as TenantIdResolver, v as TrialProjectionInput, w as TrialProjectionPort, U as USAGE_SNAPSHOT_PORT_TOKEN, x as USER_EMAIL_RESOLVER_TOKEN, y as USER_ID_RESOLVER_TOKEN, z as UserEmailResolver, B as UserIdResolver } from '../subscription-bundles.module-BQmoozm5.js';
11
- export { A as AddBundleToSubscriptionInput, B as BundlePreviewSnapshot, C as CancelBundleFromSubscriptionInput, a as CancelSubscriptionDto, b as ChangePlanDto, c as CompleteOnboardingSubscriptionDto, d as ComposedTenantAuthGuard, L as LimitsCheckRow, P as PendingPlanMaterializationService, e as PlanChangeContext, f as PlanChangePreviewDto, g as PlanChangePreviewIssue, h as PlanChangePreviewService, i as PlanChangeType, j as PlanSnapshotDto, k as PreviewPlanChangeDto, l as ProrationDto, m as ProrationInput, R as RedundantFeatureHint, S as SubscriptionBundleAddPreviewDto, n as SubscriptionBundleCancelPreviewDto, o as SubscriptionBundleConfig, p as SubscriptionBundlePreviewContext, q as SubscriptionBundlePreviewIssue, r as SubscriptionBundlePreviewService, s as SubscriptionBundlesService, T as TenantAdminGuard, t as TenantBillingController, U as UsageResponse, u as addMonths, v as computeProration, w as resolveBundleCancelEffectiveAt, x as resolveBundlePriceNet } from '../tenant-billing.controller-b1G1yB09.js';
11
+ export { A as AddBundleToSubscriptionInput, B as BundlePreviewSnapshot, C as CancelBundleFromSubscriptionInput, a as CancelSubscriptionDto, b as ChangePlanDto, c as CompleteOnboardingSubscriptionDto, d as ComposedTenantAuthGuard, L as LimitsCheckRow, P as PendingPlanMaterializationService, e as PlanChangeContext, f as PlanChangePreviewDto, g as PlanChangePreviewIssue, h as PlanChangePreviewService, i as PlanChangeType, j as PlanSnapshotDto, k as PreviewPlanChangeDto, l as ProrationDto, m as ProrationInput, R as RedundantFeatureHint, S as SubscriptionBundleAddPreviewDto, n as SubscriptionBundleCancelPreviewDto, o as SubscriptionBundleConfig, p as SubscriptionBundlePreviewContext, q as SubscriptionBundlePreviewIssue, r as SubscriptionBundlePreviewService, s as SubscriptionBundlesService, T as TenantAdminGuard, t as TenantBillingController, U as UsageResponse, u as addMonths, v as computeProration, w as resolveBundleCancelEffectiveAt, x as resolveBundlePriceNet } from '../tenant-billing.controller-DT5UrnDX.js';
12
12
  import { S as SubscriptionContractService } from '../subscription-contract.service--cm47ZJJ.js';
13
13
  import '../plan-resolution-CFCoUkrE.js';
14
- import '../service-DPZ2W1E-.js';
14
+ import '../service-D48clObR.js';
15
15
  import '../admin-audit.service-9IqXMlZm.js';
16
16
 
17
17
  /**
@@ -197,10 +197,11 @@ declare class FeatureGuard implements CanActivate {
197
197
  constructor(reflector: Reflector, entitlements: EntitlementService, config?: FeatureGuardConfig | null, upsellResolver?: UpsellOfferResolver | null);
198
198
  canActivate(context: ExecutionContext): Promise<boolean>;
199
199
  /**
200
- * Upsell response (#36): with a registered `UpsellOfferResolver` the 403
201
- * becomes machine-readable (`FeatureNotLicensedBody`), so consumer UIs can
202
- * render a purchase offer. Without a resolver the body carries the same
203
- * `code` but no offers.
200
+ * Upsell response (#36): the 403 body is always the full
201
+ * `FeatureNotLicensedBody`, so a consumer that matches on `code` can read
202
+ * `featureKey`/`offers` unconditionally. Without a registered
203
+ * `UpsellOfferResolver` there is simply nothing to offer — `offers` is
204
+ * empty, the same degradation a failing resolver produces.
204
205
  *
205
206
  * Deliberately 403 + `code` field instead of 402 — rationale in
206
207
  * `@saasicat/types` upsell.types.ts (402 is reserved / inconsistently
@@ -58,8 +58,8 @@ import {
58
58
  periodEndWithMinLead,
59
59
  resolveBundleCancelEffectiveAt,
60
60
  resolveBundlePriceNet
61
- } from "../chunk-I77VS3YH.js";
62
- import "../chunk-QLJHIIOY.js";
61
+ } from "../chunk-NZLAYCEX.js";
62
+ import "../chunk-VQQWF7MW.js";
63
63
  import "../chunk-B7NRY2LV.js";
64
64
  import {
65
65
  AUDIT_CONTEXT_RESOLVER_TOKEN,
@@ -81,7 +81,7 @@ import "../chunk-YSC46BGM.js";
81
81
  import {
82
82
  SUBSCRIPTION_BUNDLE_CONFIG_TOKEN,
83
83
  SUBSCRIPTION_BUNDLE_REPOSITORY_TOKEN
84
- } from "../chunk-F6DCLVBB.js";
84
+ } from "../chunk-AGOG5B77.js";
85
85
  import {
86
86
  PLAN_CATALOG_READ_SINK_TOKEN,
87
87
  PLAN_CATALOG_TOKEN,
@@ -98,6 +98,7 @@ import {
98
98
  isFeaturePlannedOnly
99
99
  } from "../chunk-DQKCK7DX.js";
100
100
  import "../chunk-NYLON2VC.js";
101
+ import "../chunk-EBJJNTW6.js";
101
102
  import "../chunk-WKVBNTYC.js";
102
103
  import "../chunk-L4KTQ6CL.js";
103
104
  import "../chunk-SABTXESR.js";
@@ -6,7 +6,7 @@ import {
6
6
  runSeedGateFromFile,
7
7
  seedGateExitCode,
8
8
  validateSeedAgainstSnapshot
9
- } from "../chunk-AFK2IE2L.js";
9
+ } from "../chunk-55QGAZRQ.js";
10
10
  import "../chunk-74SCJ4HF.js";
11
11
  import {
12
12
  ADVISORY_STRICT_MODE_CODES,
@@ -56,7 +56,7 @@ import {
56
56
  quotaApprovalSignature,
57
57
  validateBundleDraft,
58
58
  validatePlanDraft
59
- } from "../chunk-LXUBCLRK.js";
59
+ } from "../chunk-GC5AKN44.js";
60
60
  import "../chunk-WPUVJRIN.js";
61
61
  import "../chunk-E56W4U2P.js";
62
62
  import "../chunk-YSC46BGM.js";
@@ -6,7 +6,7 @@ import {
6
6
  CreateCheckoutOfferDto,
7
7
  UpdateCheckoutOfferDto,
8
8
  buildCheckoutOfferController
9
- } from "../chunk-ORWIGSNN.js";
9
+ } from "../chunk-XTVAFJ3P.js";
10
10
  import "../chunk-BSK6YBLI.js";
11
11
  import "../chunk-YSC46BGM.js";
12
12
  import "../chunk-L4KTQ6CL.js";
@@ -1,3 +1,6 @@
1
+ import {
2
+ codedError
3
+ } from "./chunk-EBJJNTW6.js";
1
4
  import {
2
5
  asProvider
3
6
  } from "./chunk-SABTXESR.js";
@@ -183,25 +186,17 @@ var PendingRegistrationService = class _PendingRegistrationService {
183
186
  async selectPlan(input, context) {
184
187
  const pending = await this.repo.findById(input.pendingRegistrationId);
185
188
  if (!pending) {
186
- throw new NotFoundException({
187
- code: REGISTRATION_ERROR_CODES.PENDING_REGISTRATION_NOT_FOUND
188
- });
189
+ throw new NotFoundException(codedError(REGISTRATION_ERROR_CODES.PENDING_REGISTRATION_NOT_FOUND));
189
190
  }
190
191
  if (pending.expiresAt.getTime() < Date.now()) {
191
- throw new BadRequestException({
192
- code: REGISTRATION_ERROR_CODES.PENDING_REGISTRATION_EXPIRED
193
- });
192
+ throw new BadRequestException(codedError(REGISTRATION_ERROR_CODES.PENDING_REGISTRATION_EXPIRED));
194
193
  }
195
194
  if (pending.status !== "EMAIL_VERIFIED" && pending.status !== "PLAN_SELECTED" && pending.status !== "CHECKOUT_STARTED") {
196
- throw new BadRequestException({
197
- code: REGISTRATION_ERROR_CODES.INVALID_REGISTRATION_STATE
198
- });
195
+ throw new BadRequestException(codedError(REGISTRATION_ERROR_CODES.INVALID_REGISTRATION_STATE));
199
196
  }
200
197
  const plan = await this.planCatalog.findPublicSignupPlan(input.planId);
201
198
  if (!plan) {
202
- throw new BadRequestException({
203
- code: REGISTRATION_ERROR_CODES.PLAN_NOT_AVAILABLE
204
- });
199
+ throw new BadRequestException(codedError(REGISTRATION_ERROR_CODES.PLAN_NOT_AVAILABLE));
205
200
  }
206
201
  const now = /* @__PURE__ */ new Date();
207
202
  const expiresAt = new Date(now.getTime() + PENDING_ONBOARDING_TTL_DAYS * 24 * 60 * 60 * 1e3);
@@ -224,30 +219,20 @@ var PendingRegistrationService = class _PendingRegistrationService {
224
219
  async startCheckout(input, context) {
225
220
  const pending = await this.repo.findById(input.pendingRegistrationId);
226
221
  if (!pending) {
227
- throw new NotFoundException({
228
- code: REGISTRATION_ERROR_CODES.PENDING_REGISTRATION_NOT_FOUND
229
- });
222
+ throw new NotFoundException(codedError(REGISTRATION_ERROR_CODES.PENDING_REGISTRATION_NOT_FOUND));
230
223
  }
231
224
  if (pending.expiresAt.getTime() < Date.now()) {
232
- throw new BadRequestException({
233
- code: REGISTRATION_ERROR_CODES.PENDING_REGISTRATION_EXPIRED
234
- });
225
+ throw new BadRequestException(codedError(REGISTRATION_ERROR_CODES.PENDING_REGISTRATION_EXPIRED));
235
226
  }
236
227
  if (pending.status !== "PLAN_SELECTED" && pending.status !== "CHECKOUT_STARTED") {
237
- throw new BadRequestException({
238
- code: REGISTRATION_ERROR_CODES.PLAN_NOT_SELECTED
239
- });
228
+ throw new BadRequestException(codedError(REGISTRATION_ERROR_CODES.PLAN_NOT_SELECTED));
240
229
  }
241
230
  if (!pending.selectedPlanId) {
242
- throw new BadRequestException({
243
- code: REGISTRATION_ERROR_CODES.PLAN_NOT_SELECTED
244
- });
231
+ throw new BadRequestException(codedError(REGISTRATION_ERROR_CODES.PLAN_NOT_SELECTED));
245
232
  }
246
233
  const plan = await this.planCatalog.findPublicSignupPlan(pending.selectedPlanId);
247
234
  if (!plan) {
248
- throw new BadRequestException({
249
- code: REGISTRATION_ERROR_CODES.PLAN_NOT_AVAILABLE
250
- });
235
+ throw new BadRequestException(codedError(REGISTRATION_ERROR_CODES.PLAN_NOT_AVAILABLE));
251
236
  }
252
237
  const session = await this.paymentProvider.createCheckoutSession({
253
238
  pendingRegistrationId: pending.id,
@@ -456,9 +441,7 @@ var PendingRegistrationService = class _PendingRegistrationService {
456
441
  await this.record("OTP_VERIFY_FAILED", null, context, {
457
442
  reason: "unknown_email"
458
443
  });
459
- throw new BadRequestException({
460
- code: REGISTRATION_ERROR_CODES.OTP_INVALID
461
- });
444
+ throw new BadRequestException(codedError(REGISTRATION_ERROR_CODES.OTP_INVALID));
462
445
  }
463
446
  if (pending.status !== "PENDING_EMAIL_VERIFICATION") {
464
447
  return {
@@ -471,43 +454,33 @@ var PendingRegistrationService = class _PendingRegistrationService {
471
454
  await this.record("OTP_VERIFY_FAILED", pending.id, context, {
472
455
  reason: "no_otp_set"
473
456
  });
474
- throw new BadRequestException({
475
- code: REGISTRATION_ERROR_CODES.OTP_INVALID
476
- });
457
+ throw new BadRequestException(codedError(REGISTRATION_ERROR_CODES.OTP_INVALID));
477
458
  }
478
459
  if (pending.otpExpiresAt.getTime() < Date.now()) {
479
460
  await this.record("OTP_VERIFY_FAILED", pending.id, context, {
480
461
  reason: "expired"
481
462
  });
482
- throw new BadRequestException({
483
- code: REGISTRATION_ERROR_CODES.OTP_EXPIRED
484
- });
463
+ throw new BadRequestException(codedError(REGISTRATION_ERROR_CODES.OTP_EXPIRED));
485
464
  }
486
465
  const maxAttempts = resolveOtpVerifyMaxAttempts();
487
466
  if (pending.otpAttemptCount >= maxAttempts) {
488
467
  await this.record("OTP_VERIFY_FAILED", pending.id, context, {
489
468
  reason: "locked"
490
469
  });
491
- throw new BadRequestException({
492
- code: REGISTRATION_ERROR_CODES.OTP_LOCKED
493
- });
470
+ throw new BadRequestException(codedError(REGISTRATION_ERROR_CODES.OTP_LOCKED));
494
471
  }
495
472
  const attemptCount = await this.repo.incrementOtpAttemptCount(pending.id);
496
473
  if (attemptCount > maxAttempts) {
497
474
  await this.record("OTP_VERIFY_FAILED", pending.id, context, {
498
475
  reason: "locked"
499
476
  });
500
- throw new BadRequestException({
501
- code: REGISTRATION_ERROR_CODES.OTP_LOCKED
502
- });
477
+ throw new BadRequestException(codedError(REGISTRATION_ERROR_CODES.OTP_LOCKED));
503
478
  }
504
479
  if (!verifyOtpCode(pending.otpHash, code)) {
505
480
  await this.record("OTP_VERIFY_FAILED", pending.id, context, {
506
481
  reason: "wrong_code"
507
482
  });
508
- throw new BadRequestException({
509
- code: REGISTRATION_ERROR_CODES.OTP_INVALID
510
- });
483
+ throw new BadRequestException(codedError(REGISTRATION_ERROR_CODES.OTP_INVALID));
511
484
  }
512
485
  const now = /* @__PURE__ */ new Date();
513
486
  const updated = await this.repo.update(pending.id, {
@@ -618,28 +591,20 @@ var PendingRegistrationService = class _PendingRegistrationService {
618
591
  */
619
592
  async resumeWithToken(input, context) {
620
593
  if (!this.resumeTokenSigner) {
621
- throw new BadRequestException({
622
- code: REGISTRATION_ERROR_CODES.RESUME_NOT_CONFIGURED
623
- });
594
+ throw new BadRequestException(codedError(REGISTRATION_ERROR_CODES.RESUME_NOT_CONFIGURED));
624
595
  }
625
596
  let decoded;
626
597
  try {
627
598
  decoded = await this.resumeTokenSigner.verify(input.token);
628
599
  } catch {
629
- throw new BadRequestException({
630
- code: REGISTRATION_ERROR_CODES.RESUME_TOKEN_INVALID
631
- });
600
+ throw new BadRequestException(codedError(REGISTRATION_ERROR_CODES.RESUME_TOKEN_INVALID));
632
601
  }
633
602
  const pending = await this.repo.findById(decoded.pendingRegistrationId);
634
603
  if (!pending) {
635
- throw new BadRequestException({
636
- code: REGISTRATION_ERROR_CODES.RESUME_TOKEN_INVALID
637
- });
604
+ throw new BadRequestException(codedError(REGISTRATION_ERROR_CODES.RESUME_TOKEN_INVALID));
638
605
  }
639
606
  if (pending.expiresAt.getTime() < Date.now()) {
640
- throw new BadRequestException({
641
- code: REGISTRATION_ERROR_CODES.PENDING_REGISTRATION_EXPIRED
642
- });
607
+ throw new BadRequestException(codedError(REGISTRATION_ERROR_CODES.PENDING_REGISTRATION_EXPIRED));
643
608
  }
644
609
  await this.record("REGISTRATION_NEUTRAL_REPLAY", pending.id, context, {
645
610
  via: "resume_token",
@@ -667,9 +632,7 @@ var PendingRegistrationService = class _PendingRegistrationService {
667
632
  /** Returns the (app-specific) configurator catalog. */
668
633
  async getConfiguratorCatalog() {
669
634
  if (!this.configuratorLookup) {
670
- throw new BadRequestException({
671
- code: REGISTRATION_ERROR_CODES.CONFIGURATOR_NOT_CONFIGURED
672
- });
635
+ throw new BadRequestException(codedError(REGISTRATION_ERROR_CODES.CONFIGURATOR_NOT_CONFIGURED));
673
636
  }
674
637
  return this.configuratorLookup.getCatalog();
675
638
  }
@@ -686,32 +649,22 @@ var PendingRegistrationService = class _PendingRegistrationService {
686
649
  */
687
650
  async saveConfiguration(input, context) {
688
651
  if (!this.configuratorLookup) {
689
- throw new BadRequestException({
690
- code: REGISTRATION_ERROR_CODES.CONFIGURATOR_NOT_CONFIGURED
691
- });
652
+ throw new BadRequestException(codedError(REGISTRATION_ERROR_CODES.CONFIGURATOR_NOT_CONFIGURED));
692
653
  }
693
654
  const pending = await this.repo.findById(input.pendingRegistrationId);
694
655
  if (!pending) {
695
- throw new NotFoundException({
696
- code: REGISTRATION_ERROR_CODES.PENDING_REGISTRATION_NOT_FOUND
697
- });
656
+ throw new NotFoundException(codedError(REGISTRATION_ERROR_CODES.PENDING_REGISTRATION_NOT_FOUND));
698
657
  }
699
658
  if (pending.expiresAt.getTime() < Date.now()) {
700
- throw new BadRequestException({
701
- code: REGISTRATION_ERROR_CODES.PENDING_REGISTRATION_EXPIRED
702
- });
659
+ throw new BadRequestException(codedError(REGISTRATION_ERROR_CODES.PENDING_REGISTRATION_EXPIRED));
703
660
  }
704
661
  if (pending.status !== "EMAIL_VERIFIED" && pending.status !== "PLAN_SELECTED" && pending.status !== "CHECKOUT_STARTED") {
705
- throw new BadRequestException({
706
- code: REGISTRATION_ERROR_CODES.INVALID_REGISTRATION_STATE
707
- });
662
+ throw new BadRequestException(codedError(REGISTRATION_ERROR_CODES.INVALID_REGISTRATION_STATE));
708
663
  }
709
664
  const catalog = await this.configuratorLookup.getCatalog();
710
665
  const model = catalog.models.find((m) => m.id === input.selection.modelId);
711
666
  if (!model) {
712
- throw new BadRequestException({
713
- code: REGISTRATION_ERROR_CODES.MODEL_NOT_AVAILABLE
714
- });
667
+ throw new BadRequestException(codedError(REGISTRATION_ERROR_CODES.MODEL_NOT_AVAILABLE));
715
668
  }
716
669
  let promoEval;
717
670
  if (input.selection.appliedPromoCode && this.promoPreview) {
@@ -775,28 +728,20 @@ var PendingRegistrationService = class _PendingRegistrationService {
775
728
  */
776
729
  async previewConfigPromo(pendingRegistrationId, code) {
777
730
  if (!this.configuratorLookup) {
778
- throw new BadRequestException({
779
- code: REGISTRATION_ERROR_CODES.CONFIGURATOR_NOT_CONFIGURED
780
- });
731
+ throw new BadRequestException(codedError(REGISTRATION_ERROR_CODES.CONFIGURATOR_NOT_CONFIGURED));
781
732
  }
782
733
  const pending = await this.repo.findById(pendingRegistrationId);
783
734
  if (!pending) {
784
- throw new NotFoundException({
785
- code: REGISTRATION_ERROR_CODES.PENDING_REGISTRATION_NOT_FOUND
786
- });
735
+ throw new NotFoundException(codedError(REGISTRATION_ERROR_CODES.PENDING_REGISTRATION_NOT_FOUND));
787
736
  }
788
737
  const config = pending.configJson;
789
738
  if (!config) {
790
- throw new BadRequestException({
791
- code: REGISTRATION_ERROR_CODES.CONFIG_NOT_SAVED
792
- });
739
+ throw new BadRequestException(codedError(REGISTRATION_ERROR_CODES.CONFIG_NOT_SAVED));
793
740
  }
794
741
  const catalog = await this.configuratorLookup.getCatalog();
795
742
  const model = catalog.models.find((m) => m.id === config.modelId);
796
743
  if (!model) {
797
- throw new BadRequestException({
798
- code: REGISTRATION_ERROR_CODES.MODEL_NOT_AVAILABLE
799
- });
744
+ throw new BadRequestException(codedError(REGISTRATION_ERROR_CODES.MODEL_NOT_AVAILABLE));
800
745
  }
801
746
  let promoEval;
802
747
  if (code && this.promoPreview) {
@@ -1030,9 +975,14 @@ var BaseIpRateLimitGuard = class {
1030
975
  const key = ipFingerprint(req);
1031
976
  if (this.exceeded(key)) {
1032
977
  throw new HttpException({
1033
- code: REGISTRATION_ERROR_CODES2.RATE_LIMITED,
1034
- reason: this.options.name,
1035
- retryAfterSeconds: Math.ceil(this.options.windowMs / 1e3)
978
+ ...codedError(REGISTRATION_ERROR_CODES2.RATE_LIMITED, {
979
+ retryAfterSeconds: Math.ceil(this.options.windowMs / 1e3)
980
+ }),
981
+ retryAfterSeconds: Math.ceil(this.options.windowMs / 1e3),
982
+ // Superseded by `code`. Note this carries the limiter name,
983
+ // not the code — the two emitters of RATE_LIMITED disagree
984
+ // on what `reason` means, which is why it is going away.
985
+ reason: this.options.name
1036
986
  }, HttpStatus.TOO_MANY_REQUESTS);
1037
987
  }
1038
988
  return true;
@@ -8,13 +8,16 @@ import {
8
8
  PROMO_SUBSCRIPTION_LOOKUP_TOKEN,
9
9
  PROMO_TRANSACTION_RUNNER_TOKEN,
10
10
  PromoCodesService
11
- } from "./chunk-QLJHIIOY.js";
11
+ } from "./chunk-VQQWF7MW.js";
12
12
  import {
13
13
  AdminAuditService
14
14
  } from "./chunk-E56W4U2P.js";
15
15
  import {
16
16
  SaaSiCatPublicRoute
17
17
  } from "./chunk-YSC46BGM.js";
18
+ import {
19
+ codedError
20
+ } from "./chunk-EBJJNTW6.js";
18
21
  import {
19
22
  asProvider
20
23
  } from "./chunk-SABTXESR.js";
@@ -96,12 +99,15 @@ var IP_WINDOW_MS = 6e4;
96
99
  var IP_LIMIT = 20;
97
100
  var SESSION_WINDOW_MS = 60 * 6e4;
98
101
  var SESSION_LIMIT = 50;
99
- function rateLimited() {
102
+ function rateLimited(windowMs) {
100
103
  return new HttpException({
104
+ ...codedError(REGISTRATION_ERROR_CODES.RATE_LIMITED, {
105
+ retryAfterSeconds: Math.ceil(windowMs / 1e3)
106
+ }),
107
+ retryAfterSeconds: Math.ceil(windowMs / 1e3),
101
108
  valid: false,
102
109
  // Superseded by `code` — kept until all consumers read `code`.
103
- reason: "RATE_LIMITED",
104
- code: REGISTRATION_ERROR_CODES.RATE_LIMITED
110
+ reason: "RATE_LIMITED"
105
111
  }, HttpStatus.TOO_MANY_REQUESTS);
106
112
  }
107
113
  __name(rateLimited, "rateLimited");
@@ -116,10 +122,10 @@ var PromoCodeRateLimitGuard = class {
116
122
  const ipKey = ipFingerprint(req);
117
123
  const sessionKey = req.user?.id ?? null;
118
124
  if (this.exceeded(this.ipBuckets, ipKey, IP_WINDOW_MS, IP_LIMIT)) {
119
- throw rateLimited();
125
+ throw rateLimited(IP_WINDOW_MS);
120
126
  }
121
127
  if (sessionKey && this.exceeded(this.sessionBuckets, sessionKey, SESSION_WINDOW_MS, SESSION_LIMIT)) {
122
- throw rateLimited();
128
+ throw rateLimited(SESSION_WINDOW_MS);
123
129
  }
124
130
  return true;
125
131
  }
@@ -6,7 +6,7 @@ import {
6
6
  ADVISORY_STRICT_MODE_CODES,
7
7
  validateBundleDraft,
8
8
  validatePlanDraft
9
- } from "./chunk-LXUBCLRK.js";
9
+ } from "./chunk-GC5AKN44.js";
10
10
  import {
11
11
  __name
12
12
  } from "./chunk-SHUYVCID.js";
@@ -5,6 +5,9 @@ import {
5
5
  import {
6
6
  SUBSCRIPTION_CONTRACT_REPOSITORY_TOKEN
7
7
  } from "./chunk-NYLON2VC.js";
8
+ import {
9
+ codedError
10
+ } from "./chunk-EBJJNTW6.js";
8
11
  import {
9
12
  ENTITLEMENT_RESOLUTION_CONFIG_TOKEN,
10
13
  ENTITLEMENT_SERVICE_TOKEN,
@@ -304,7 +307,7 @@ function isLimitExceededError(err) {
304
307
  __name(isLimitExceededError, "isLimitExceededError");
305
308
 
306
309
  // src/entitlement/service.ts
307
- import { Inject, Injectable, NotFoundException, Optional } from "@nestjs/common";
310
+ import { Inject, Injectable, InternalServerErrorException, NotFoundException, Optional } from "@nestjs/common";
308
311
  import { BILLING_ERROR_CODES } from "@saasicat/types";
309
312
 
310
313
  // src/billing/subscription-bundles.tokens.ts
@@ -484,7 +487,10 @@ var EntitlementService = class {
484
487
  const limits = await this.deriveLimits(sub, now, tx);
485
488
  const max = limits.quotas[input.dimension];
486
489
  if (max === void 0) {
487
- throw new Error(`Quota-Dimension "${input.dimension}" is not in the plan catalog (${this.catalog.projectKey}).`);
490
+ throw new InternalServerErrorException(codedError(BILLING_ERROR_CODES.QUOTA_DIMENSION_UNKNOWN, {
491
+ dimension: input.dimension,
492
+ projectKey: this.catalog.projectKey
493
+ }));
488
494
  }
489
495
  const used = await input.currentUsage(tx, input.tenantId);
490
496
  if (max !== -1 && used + delta > max) {
@@ -0,0 +1,22 @@
1
+ import {
2
+ __name
3
+ } from "./chunk-SHUYVCID.js";
4
+
5
+ // src/errors/coded-error.ts
6
+ import { ERROR_MESSAGES_EN, formatErrorMessage } from "@saasicat/types";
7
+ function codedError(code, params) {
8
+ const message = formatErrorMessage(ERROR_MESSAGES_EN[code], params ?? {});
9
+ return params ? {
10
+ code,
11
+ message,
12
+ params
13
+ } : {
14
+ code,
15
+ message
16
+ };
17
+ }
18
+ __name(codedError, "codedError");
19
+
20
+ export {
21
+ codedError
22
+ };
@@ -7,6 +7,9 @@ import {
7
7
  import {
8
8
  SaaSiCatPublicRoute
9
9
  } from "./chunk-YSC46BGM.js";
10
+ import {
11
+ codedError
12
+ } from "./chunk-EBJJNTW6.js";
10
13
  import {
11
14
  asProvider
12
15
  } from "./chunk-SABTXESR.js";
@@ -199,7 +202,7 @@ var MfaGuard = class _MfaGuard {
199
202
  const user = request.user;
200
203
  if (!user) {
201
204
  throw new UnauthorizedException({
202
- code: AUTH_ERROR_CODES2.NOT_AUTHENTICATED,
205
+ ...codedError(AUTH_ERROR_CODES2.NOT_AUTHENTICATED),
203
206
  // `reason` is superseded by `code` and will be removed.
204
207
  reason: "NOT_AUTHENTICATED"
205
208
  });
@@ -330,7 +330,7 @@ var BundlesService = class _BundlesService {
330
330
  code: CATALOG_ERROR_CODES.BUNDLE_VERSION_NOT_FOUND,
331
331
  message: `BundleVersion '${versionId}' not found`,
332
332
  params: {
333
- versionId
333
+ bundleVersionId: versionId
334
334
  }
335
335
  });
336
336
  }
@@ -364,7 +364,7 @@ var BundlesService = class _BundlesService {
364
364
  code: CATALOG_ERROR_CODES.BUNDLE_VERSION_NOT_FOUND,
365
365
  message: `BundleVersion '${versionId}' not found`,
366
366
  params: {
367
- versionId
367
+ bundleVersionId: versionId
368
368
  }
369
369
  });
370
370
  }
@@ -461,7 +461,7 @@ var BundlesService = class _BundlesService {
461
461
  code: CATALOG_ERROR_CODES.BUNDLE_VERSION_NOT_FOUND,
462
462
  message: `BundleVersion '${versionId}' not found`,
463
463
  params: {
464
- versionId
464
+ bundleVersionId: versionId
465
465
  }
466
466
  });
467
467
  }
@@ -510,7 +510,7 @@ var BundlesService = class _BundlesService {
510
510
  code: CATALOG_ERROR_CODES.BUNDLE_VERSION_NOT_FOUND,
511
511
  message: `BundleVersion '${versionId}' not found`,
512
512
  params: {
513
- versionId
513
+ bundleVersionId: versionId
514
514
  }
515
515
  });
516
516
  }
@@ -750,9 +750,10 @@ var BundlesService = class _BundlesService {
750
750
  throw new UnprocessableEntityException({
751
751
  code,
752
752
  message: `${field} '${input}' is not a valid date`,
753
+ // Keyed by the field name so these params match the publish
754
+ // path, where the same codes carry `validFrom` / `validUntil`.
753
755
  params: {
754
- field,
755
- value: input
756
+ [field]: input
756
757
  }
757
758
  });
758
759
  }
@@ -3409,7 +3410,7 @@ var PlanVersionsService = class _PlanVersionsService {
3409
3410
  }
3410
3411
  if (existing.publishedAt === null) {
3411
3412
  throw new UnprocessableEntityException4({
3412
- code: CATALOG_ERROR_CODES6.PLAN_VERSION_NOT_LIVE,
3413
+ code: CATALOG_ERROR_CODES6.PLAN_VERSION_NOT_PUBLISHED,
3413
3414
  message: `PlanVersion '${versionId}' is not published and cannot be terminated.`,
3414
3415
  params: {
3415
3416
  versionId
@@ -3418,7 +3419,7 @@ var PlanVersionsService = class _PlanVersionsService {
3418
3419
  }
3419
3420
  if (existing.supersededAt !== null) {
3420
3421
  throw new UnprocessableEntityException4({
3421
- code: CATALOG_ERROR_CODES6.PLAN_VERSION_NOT_LIVE,
3422
+ code: CATALOG_ERROR_CODES6.PLAN_VERSION_SUPERSEDED,
3422
3423
  message: `PlanVersion '${versionId}' has already been superseded by a newer version (supersededAt) and cannot be terminated.`,
3423
3424
  params: {
3424
3425
  versionId