@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.
- package/dist/_entries.cjs +337 -376
- package/dist/admin/index.js +2 -1
- package/dist/billing/index.d.cts +7 -6
- package/dist/billing/index.d.ts +7 -6
- package/dist/billing/index.js +4 -3
- package/dist/catalog/index.js +2 -2
- package/dist/checkout-offer/index.js +1 -1
- package/dist/{chunk-6WA4X5U3.js → chunk-3DRZ5GCP.js} +40 -90
- package/dist/{chunk-EUU3B6NB.js → chunk-4O6R6ARQ.js} +12 -6
- package/dist/{chunk-AFK2IE2L.js → chunk-55QGAZRQ.js} +1 -1
- package/dist/{chunk-F6DCLVBB.js → chunk-AGOG5B77.js} +8 -2
- package/dist/chunk-EBJJNTW6.js +22 -0
- package/dist/{chunk-7JIHG53V.js → chunk-FYWLPYBK.js} +4 -1
- package/dist/{chunk-LXUBCLRK.js → chunk-GC5AKN44.js} +9 -8
- package/dist/{chunk-I77VS3YH.js → chunk-NZLAYCEX.js} +16 -22
- package/dist/{chunk-QLJHIIOY.js → chunk-VQQWF7MW.js} +7 -3
- package/dist/{chunk-ORWIGSNN.js → chunk-XTVAFJ3P.js} +2 -1
- package/dist/{chunk-KUDKD7ZG.js → chunk-ZH5PYEMI.js} +19 -21
- package/dist/{define-saasicat-CqkhnHn-.d.ts → define-saasicat-B20Ms8UC.d.ts} +1 -1
- package/dist/{define-saasicat-D8c6Bs4T.d.cts → define-saasicat-Cf9DpEsY.d.cts} +1 -1
- package/dist/{enforce-quota.interceptor-Dp8wTYro.d.ts → enforce-quota.interceptor-CUyfR4cQ.d.ts} +1 -1
- package/dist/{enforce-quota.interceptor-Dk0eEtzV.d.cts → enforce-quota.interceptor-Dtg5WdFj.d.cts} +1 -1
- package/dist/entitlement/index.js +2 -1
- package/dist/index.d.cts +27 -8
- package/dist/index.d.ts +27 -8
- package/dist/index.js +14 -10
- package/dist/{module-BwD-wAor.d.ts → module-BE17XFLg.d.ts} +1 -1
- package/dist/{module-DsDkbawm.d.cts → module-CHMIyWlf.d.cts} +1 -1
- package/dist/platform/index.d.cts +8 -8
- package/dist/platform/index.d.ts +8 -8
- package/dist/platform/index.js +9 -8
- package/dist/promo/index.d.cts +3 -3
- package/dist/promo/index.d.ts +3 -3
- package/dist/promo/index.js +3 -2
- package/dist/registration/index.js +2 -1
- package/dist/{saas-platform.module-DducnQ9s.d.cts → saas-platform.module-BfaYfL-G.d.cts} +1 -1
- package/dist/{saas-platform.module-BQmrDGVU.d.ts → saas-platform.module-DChhDrR-.d.ts} +1 -1
- package/dist/{service-DPZ2W1E-.d.ts → service-D48clObR.d.cts} +1 -1
- package/dist/{service-DPZ2W1E-.d.cts → service-D48clObR.d.ts} +1 -1
- package/dist/{tenant-billing.controller-m42YQ_Ei.d.cts → tenant-billing.controller-BIF-mstX.d.cts} +1 -1
- package/dist/{tenant-billing.controller-b1G1yB09.d.ts → tenant-billing.controller-DT5UrnDX.d.ts} +1 -1
- package/dist/testing/index.d.cts +5 -5
- package/dist/testing/index.d.ts +5 -5
- package/dist/testing/index.js +9 -8
- package/package.json +3 -3
package/dist/admin/index.js
CHANGED
|
@@ -25,7 +25,7 @@ import {
|
|
|
25
25
|
SuperAdminGuard,
|
|
26
26
|
SuspendTenantDto,
|
|
27
27
|
buildAdminStatsController
|
|
28
|
-
} from "../chunk-
|
|
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 {
|
package/dist/billing/index.d.cts
CHANGED
|
@@ -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-
|
|
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-
|
|
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):
|
|
201
|
-
*
|
|
202
|
-
*
|
|
203
|
-
* `
|
|
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
|
package/dist/billing/index.d.ts
CHANGED
|
@@ -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-
|
|
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-
|
|
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):
|
|
201
|
-
*
|
|
202
|
-
*
|
|
203
|
-
* `
|
|
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
|
package/dist/billing/index.js
CHANGED
|
@@ -58,8 +58,8 @@ import {
|
|
|
58
58
|
periodEndWithMinLead,
|
|
59
59
|
resolveBundleCancelEffectiveAt,
|
|
60
60
|
resolveBundlePriceNet
|
|
61
|
-
} from "../chunk-
|
|
62
|
-
import "../chunk-
|
|
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-
|
|
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";
|
package/dist/catalog/index.js
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
runSeedGateFromFile,
|
|
7
7
|
seedGateExitCode,
|
|
8
8
|
validateSeedAgainstSnapshot
|
|
9
|
-
} from "../chunk-
|
|
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-
|
|
59
|
+
} from "../chunk-GC5AKN44.js";
|
|
60
60
|
import "../chunk-WPUVJRIN.js";
|
|
61
61
|
import "../chunk-E56W4U2P.js";
|
|
62
62
|
import "../chunk-YSC46BGM.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
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
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-
|
|
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
|
}
|
|
@@ -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
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|