@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/_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
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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."
|
|
@@ -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
|
|
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:
|
|
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:
|
|
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: () =>
|
|
1672
|
-
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
|
|
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
|
|
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
|
|
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:
|
|
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:
|
|
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
|
|
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) {
|
|
@@ -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:
|
|
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:
|
|
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:
|
|
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):
|
|
2667
|
-
*
|
|
2668
|
-
*
|
|
2669
|
-
* `
|
|
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:
|
|
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
|
|
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,
|
|
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
|
|
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:
|
|
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:
|
|
2897
|
+
reason: import_types9.BILLING_ERROR_CODES.LIMIT_EXCEEDED,
|
|
2889
2898
|
dimension: exception.dimension,
|
|
2890
2899
|
used: exception.used,
|
|
2891
2900
|
max: exception.max,
|
|
@@ -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
|
|
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);
|
|
@@ -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,
|
|
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,
|
|
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
|
|
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:
|
|
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
|
|
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:
|
|
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:
|
|
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
|
|
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:
|
|
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:
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
5205
|
+
code: import_types15.BILLING_ERROR_CODES.SUBSCRIPTION_NOT_FOUND,
|
|
5193
5206
|
message: "Subscription not found",
|
|
5194
5207
|
params: {
|
|
5195
|
-
|
|
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:
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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,
|
|
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,
|
|
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
|
|
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:
|
|
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(
|
|
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:
|
|
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:
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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,
|
|
7814
|
+
const { editable } = (0, import_types20.isVersionEditable)(annotated);
|
|
7802
7815
|
if (!editable) {
|
|
7803
7816
|
throw new import_common43.UnprocessableEntityException({
|
|
7804
|
-
code:
|
|
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
|
|
@@ -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:
|
|
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:
|
|
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:
|
|
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:
|
|
7891
|
+
code: import_types20.CATALOG_ERROR_CODES.BUNDLE_VERSION_VALID_FROM_REQUIRED,
|
|
7879
7892
|
message: "validFrom must be set when publishing (on the draft or the publish call). SPEC_V2 \xA74.2."
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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,
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
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,
|
|
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,
|
|
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,
|
|
10050
|
-
const yearlyPromo = (0,
|
|
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,
|
|
10054
|
-
const yearlyResult = (0,
|
|
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,
|
|
@@ -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
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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,
|
|
10575
|
+
const { editable } = (0, import_types26.isVersionEditable)(annotated);
|
|
10562
10576
|
if (!editable) {
|
|
10563
10577
|
throw new import_common55.UnprocessableEntityException({
|
|
10564
|
-
code:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
10638
|
+
code: import_types26.CATALOG_ERROR_CODES.PLAN_VERSION_VALID_FROM_REQUIRED,
|
|
10625
10639
|
message: "validFrom must be set when publishing (on the draft or the publish call). SPEC_V2 \xA74.2."
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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,
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
});
|
|
@@ -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
|
|
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:
|
|
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,
|
|
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,
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
14543
|
+
if ((0, import_types29.isPlatformUserExistsError)(err)) {
|
|
14528
14544
|
throw new import_common71.ConflictException({
|
|
14529
|
-
code:
|
|
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:
|
|
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
|
|
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:
|
|
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:
|
|
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
|
-
|
|
14915
|
-
code:
|
|
14916
|
-
|
|
14917
|
-
|
|
14918
|
-
|
|
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
|
}
|
|
@@ -15397,7 +15410,7 @@ var SaasPlatformModule = class _SaasPlatformModule {
|
|
|
15397
15410
|
throw new Error(`SaasPlatformModule.forRoot: entitlement active or required by the standard stack, but adapters are missing: ${missing.join(", ")}`);
|
|
15398
15411
|
}
|
|
15399
15412
|
if (persistence) {
|
|
15400
|
-
(0,
|
|
15413
|
+
(0, import_types31.assertPersistenceCapabilities)(persistence.capabilities, {
|
|
15401
15414
|
transactions: true,
|
|
15402
15415
|
pessimisticLocking: true
|
|
15403
15416
|
}, "SaasPlatformModule entitlement (transactional enforceLimit)");
|
|
@@ -15853,8 +15866,8 @@ var REGISTRATION_CONFIG_TOKEN = /* @__PURE__ */ Symbol.for("saas-platform/Regist
|
|
|
15853
15866
|
|
|
15854
15867
|
// src/registration/pending-registration.service.ts
|
|
15855
15868
|
var import_common80 = require("@nestjs/common");
|
|
15856
|
-
var import_types31 = require("@saasicat/types");
|
|
15857
15869
|
var import_types32 = require("@saasicat/types");
|
|
15870
|
+
var import_types33 = require("@saasicat/types");
|
|
15858
15871
|
|
|
15859
15872
|
// src/registration/pricing.ts
|
|
15860
15873
|
function round24(value) {
|
|
@@ -15982,28 +15995,20 @@ var PendingRegistrationService = class _PendingRegistrationService {
|
|
|
15982
15995
|
async selectPlan(input, context) {
|
|
15983
15996
|
const pending = await this.repo.findById(input.pendingRegistrationId);
|
|
15984
15997
|
if (!pending) {
|
|
15985
|
-
throw new import_common80.NotFoundException(
|
|
15986
|
-
code: import_types31.REGISTRATION_ERROR_CODES.PENDING_REGISTRATION_NOT_FOUND
|
|
15987
|
-
});
|
|
15998
|
+
throw new import_common80.NotFoundException(codedError(import_types32.REGISTRATION_ERROR_CODES.PENDING_REGISTRATION_NOT_FOUND));
|
|
15988
15999
|
}
|
|
15989
16000
|
if (pending.expiresAt.getTime() < Date.now()) {
|
|
15990
|
-
throw new import_common80.BadRequestException(
|
|
15991
|
-
code: import_types31.REGISTRATION_ERROR_CODES.PENDING_REGISTRATION_EXPIRED
|
|
15992
|
-
});
|
|
16001
|
+
throw new import_common80.BadRequestException(codedError(import_types32.REGISTRATION_ERROR_CODES.PENDING_REGISTRATION_EXPIRED));
|
|
15993
16002
|
}
|
|
15994
16003
|
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
|
-
});
|
|
16004
|
+
throw new import_common80.BadRequestException(codedError(import_types32.REGISTRATION_ERROR_CODES.INVALID_REGISTRATION_STATE));
|
|
15998
16005
|
}
|
|
15999
16006
|
const plan = await this.planCatalog.findPublicSignupPlan(input.planId);
|
|
16000
16007
|
if (!plan) {
|
|
16001
|
-
throw new import_common80.BadRequestException(
|
|
16002
|
-
code: import_types31.REGISTRATION_ERROR_CODES.PLAN_NOT_AVAILABLE
|
|
16003
|
-
});
|
|
16008
|
+
throw new import_common80.BadRequestException(codedError(import_types32.REGISTRATION_ERROR_CODES.PLAN_NOT_AVAILABLE));
|
|
16004
16009
|
}
|
|
16005
16010
|
const now = /* @__PURE__ */ new Date();
|
|
16006
|
-
const expiresAt = new Date(now.getTime() +
|
|
16011
|
+
const expiresAt = new Date(now.getTime() + import_types33.PENDING_ONBOARDING_TTL_DAYS * 24 * 60 * 60 * 1e3);
|
|
16007
16012
|
const updated = await this.repo.update(pending.id, {
|
|
16008
16013
|
status: "PLAN_SELECTED",
|
|
16009
16014
|
currentStep: 4,
|
|
@@ -16016,37 +16021,27 @@ var PendingRegistrationService = class _PendingRegistrationService {
|
|
|
16016
16021
|
return {
|
|
16017
16022
|
pendingRegistrationId: updated.id,
|
|
16018
16023
|
status: updated.status,
|
|
16019
|
-
nextStep:
|
|
16024
|
+
nextStep: import_types33.REGISTRATION_STEP_BY_STATUS[updated.status],
|
|
16020
16025
|
selectedPlanId: updated.selectedPlanId ?? plan.id
|
|
16021
16026
|
};
|
|
16022
16027
|
}
|
|
16023
16028
|
async startCheckout(input, context) {
|
|
16024
16029
|
const pending = await this.repo.findById(input.pendingRegistrationId);
|
|
16025
16030
|
if (!pending) {
|
|
16026
|
-
throw new import_common80.NotFoundException(
|
|
16027
|
-
code: import_types31.REGISTRATION_ERROR_CODES.PENDING_REGISTRATION_NOT_FOUND
|
|
16028
|
-
});
|
|
16031
|
+
throw new import_common80.NotFoundException(codedError(import_types32.REGISTRATION_ERROR_CODES.PENDING_REGISTRATION_NOT_FOUND));
|
|
16029
16032
|
}
|
|
16030
16033
|
if (pending.expiresAt.getTime() < Date.now()) {
|
|
16031
|
-
throw new import_common80.BadRequestException(
|
|
16032
|
-
code: import_types31.REGISTRATION_ERROR_CODES.PENDING_REGISTRATION_EXPIRED
|
|
16033
|
-
});
|
|
16034
|
+
throw new import_common80.BadRequestException(codedError(import_types32.REGISTRATION_ERROR_CODES.PENDING_REGISTRATION_EXPIRED));
|
|
16034
16035
|
}
|
|
16035
16036
|
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
|
-
});
|
|
16037
|
+
throw new import_common80.BadRequestException(codedError(import_types32.REGISTRATION_ERROR_CODES.PLAN_NOT_SELECTED));
|
|
16039
16038
|
}
|
|
16040
16039
|
if (!pending.selectedPlanId) {
|
|
16041
|
-
throw new import_common80.BadRequestException(
|
|
16042
|
-
code: import_types31.REGISTRATION_ERROR_CODES.PLAN_NOT_SELECTED
|
|
16043
|
-
});
|
|
16040
|
+
throw new import_common80.BadRequestException(codedError(import_types32.REGISTRATION_ERROR_CODES.PLAN_NOT_SELECTED));
|
|
16044
16041
|
}
|
|
16045
16042
|
const plan = await this.planCatalog.findPublicSignupPlan(pending.selectedPlanId);
|
|
16046
16043
|
if (!plan) {
|
|
16047
|
-
throw new import_common80.BadRequestException(
|
|
16048
|
-
code: import_types31.REGISTRATION_ERROR_CODES.PLAN_NOT_AVAILABLE
|
|
16049
|
-
});
|
|
16044
|
+
throw new import_common80.BadRequestException(codedError(import_types32.REGISTRATION_ERROR_CODES.PLAN_NOT_AVAILABLE));
|
|
16050
16045
|
}
|
|
16051
16046
|
const session = await this.paymentProvider.createCheckoutSession({
|
|
16052
16047
|
pendingRegistrationId: pending.id,
|
|
@@ -16056,7 +16051,7 @@ var PendingRegistrationService = class _PendingRegistrationService {
|
|
|
16056
16051
|
cancelUrl: input.cancelUrl
|
|
16057
16052
|
});
|
|
16058
16053
|
const now = /* @__PURE__ */ new Date();
|
|
16059
|
-
const expiresAt = new Date(now.getTime() +
|
|
16054
|
+
const expiresAt = new Date(now.getTime() + import_types33.PENDING_CHECKOUT_TTL_DAYS * 24 * 60 * 60 * 1e3);
|
|
16060
16055
|
const updated = await this.repo.update(pending.id, {
|
|
16061
16056
|
status: "CHECKOUT_STARTED",
|
|
16062
16057
|
currentStep: 4,
|
|
@@ -16071,7 +16066,7 @@ var PendingRegistrationService = class _PendingRegistrationService {
|
|
|
16071
16066
|
return {
|
|
16072
16067
|
pendingRegistrationId: updated.id,
|
|
16073
16068
|
status: updated.status,
|
|
16074
|
-
nextStep:
|
|
16069
|
+
nextStep: import_types33.REGISTRATION_STEP_BY_STATUS[updated.status],
|
|
16075
16070
|
checkoutSessionId: session.sessionId,
|
|
16076
16071
|
checkoutUrl: session.checkoutUrl
|
|
16077
16072
|
};
|
|
@@ -16227,8 +16222,8 @@ var PendingRegistrationService = class _PendingRegistrationService {
|
|
|
16227
16222
|
const code = generateOtpCode();
|
|
16228
16223
|
const otpHash = hashOtpCode(code);
|
|
16229
16224
|
const now = /* @__PURE__ */ new Date();
|
|
16230
|
-
const otpExpiresAt = new Date(now.getTime() +
|
|
16231
|
-
const expiresAt = new Date(now.getTime() +
|
|
16225
|
+
const otpExpiresAt = new Date(now.getTime() + import_types33.OTP_TTL_MINUTES * 60 * 1e3);
|
|
16226
|
+
const expiresAt = new Date(now.getTime() + import_types33.PENDING_EMAIL_TTL_HOURS * 60 * 60 * 1e3);
|
|
16232
16227
|
const pending = await this.repo.create({
|
|
16233
16228
|
tenantName: input.tenantName.trim(),
|
|
16234
16229
|
tenantSlug: slug,
|
|
@@ -16255,14 +16250,12 @@ var PendingRegistrationService = class _PendingRegistrationService {
|
|
|
16255
16250
|
await this.record("OTP_VERIFY_FAILED", null, context, {
|
|
16256
16251
|
reason: "unknown_email"
|
|
16257
16252
|
});
|
|
16258
|
-
throw new import_common80.BadRequestException(
|
|
16259
|
-
code: import_types31.REGISTRATION_ERROR_CODES.OTP_INVALID
|
|
16260
|
-
});
|
|
16253
|
+
throw new import_common80.BadRequestException(codedError(import_types32.REGISTRATION_ERROR_CODES.OTP_INVALID));
|
|
16261
16254
|
}
|
|
16262
16255
|
if (pending.status !== "PENDING_EMAIL_VERIFICATION") {
|
|
16263
16256
|
return {
|
|
16264
16257
|
status: pending.status,
|
|
16265
|
-
nextStep:
|
|
16258
|
+
nextStep: import_types33.REGISTRATION_STEP_BY_STATUS[pending.status],
|
|
16266
16259
|
pendingRegistrationId: pending.id
|
|
16267
16260
|
};
|
|
16268
16261
|
}
|
|
@@ -16270,43 +16263,33 @@ var PendingRegistrationService = class _PendingRegistrationService {
|
|
|
16270
16263
|
await this.record("OTP_VERIFY_FAILED", pending.id, context, {
|
|
16271
16264
|
reason: "no_otp_set"
|
|
16272
16265
|
});
|
|
16273
|
-
throw new import_common80.BadRequestException(
|
|
16274
|
-
code: import_types31.REGISTRATION_ERROR_CODES.OTP_INVALID
|
|
16275
|
-
});
|
|
16266
|
+
throw new import_common80.BadRequestException(codedError(import_types32.REGISTRATION_ERROR_CODES.OTP_INVALID));
|
|
16276
16267
|
}
|
|
16277
16268
|
if (pending.otpExpiresAt.getTime() < Date.now()) {
|
|
16278
16269
|
await this.record("OTP_VERIFY_FAILED", pending.id, context, {
|
|
16279
16270
|
reason: "expired"
|
|
16280
16271
|
});
|
|
16281
|
-
throw new import_common80.BadRequestException(
|
|
16282
|
-
code: import_types31.REGISTRATION_ERROR_CODES.OTP_EXPIRED
|
|
16283
|
-
});
|
|
16272
|
+
throw new import_common80.BadRequestException(codedError(import_types32.REGISTRATION_ERROR_CODES.OTP_EXPIRED));
|
|
16284
16273
|
}
|
|
16285
16274
|
const maxAttempts = resolveOtpVerifyMaxAttempts();
|
|
16286
16275
|
if (pending.otpAttemptCount >= maxAttempts) {
|
|
16287
16276
|
await this.record("OTP_VERIFY_FAILED", pending.id, context, {
|
|
16288
16277
|
reason: "locked"
|
|
16289
16278
|
});
|
|
16290
|
-
throw new import_common80.BadRequestException(
|
|
16291
|
-
code: import_types31.REGISTRATION_ERROR_CODES.OTP_LOCKED
|
|
16292
|
-
});
|
|
16279
|
+
throw new import_common80.BadRequestException(codedError(import_types32.REGISTRATION_ERROR_CODES.OTP_LOCKED));
|
|
16293
16280
|
}
|
|
16294
16281
|
const attemptCount = await this.repo.incrementOtpAttemptCount(pending.id);
|
|
16295
16282
|
if (attemptCount > maxAttempts) {
|
|
16296
16283
|
await this.record("OTP_VERIFY_FAILED", pending.id, context, {
|
|
16297
16284
|
reason: "locked"
|
|
16298
16285
|
});
|
|
16299
|
-
throw new import_common80.BadRequestException(
|
|
16300
|
-
code: import_types31.REGISTRATION_ERROR_CODES.OTP_LOCKED
|
|
16301
|
-
});
|
|
16286
|
+
throw new import_common80.BadRequestException(codedError(import_types32.REGISTRATION_ERROR_CODES.OTP_LOCKED));
|
|
16302
16287
|
}
|
|
16303
16288
|
if (!verifyOtpCode(pending.otpHash, code)) {
|
|
16304
16289
|
await this.record("OTP_VERIFY_FAILED", pending.id, context, {
|
|
16305
16290
|
reason: "wrong_code"
|
|
16306
16291
|
});
|
|
16307
|
-
throw new import_common80.BadRequestException(
|
|
16308
|
-
code: import_types31.REGISTRATION_ERROR_CODES.OTP_INVALID
|
|
16309
|
-
});
|
|
16292
|
+
throw new import_common80.BadRequestException(codedError(import_types32.REGISTRATION_ERROR_CODES.OTP_INVALID));
|
|
16310
16293
|
}
|
|
16311
16294
|
const now = /* @__PURE__ */ new Date();
|
|
16312
16295
|
const updated = await this.repo.update(pending.id, {
|
|
@@ -16315,12 +16298,12 @@ var PendingRegistrationService = class _PendingRegistrationService {
|
|
|
16315
16298
|
emailVerifiedAt: now,
|
|
16316
16299
|
otpHash: null,
|
|
16317
16300
|
otpExpiresAt: null,
|
|
16318
|
-
expiresAt: new Date(now.getTime() +
|
|
16301
|
+
expiresAt: new Date(now.getTime() + import_types33.PENDING_ONBOARDING_TTL_DAYS * 24 * 60 * 60 * 1e3)
|
|
16319
16302
|
});
|
|
16320
16303
|
await this.record("OTP_VERIFIED", updated.id, context);
|
|
16321
16304
|
return {
|
|
16322
16305
|
status: updated.status,
|
|
16323
|
-
nextStep:
|
|
16306
|
+
nextStep: import_types33.REGISTRATION_STEP_BY_STATUS[updated.status],
|
|
16324
16307
|
pendingRegistrationId: updated.id
|
|
16325
16308
|
};
|
|
16326
16309
|
}
|
|
@@ -16345,15 +16328,15 @@ var PendingRegistrationService = class _PendingRegistrationService {
|
|
|
16345
16328
|
}
|
|
16346
16329
|
async regenerateOtp(pending) {
|
|
16347
16330
|
const now = /* @__PURE__ */ new Date();
|
|
16348
|
-
const windowMs =
|
|
16331
|
+
const windowMs = import_types33.OTP_RATE_LIMIT_WINDOW_MINUTES * 60 * 1e3;
|
|
16349
16332
|
const withinWindow = pending.lastOtpSentAt !== null && now.getTime() - pending.lastOtpSentAt.getTime() < windowMs;
|
|
16350
|
-
if (withinWindow && pending.otpSendCount >=
|
|
16333
|
+
if (withinWindow && pending.otpSendCount >= import_types33.OTP_RATE_LIMIT_MAX_SENDS) {
|
|
16351
16334
|
this.logger.warn(`OTP rate limit reached for ${pending.email} \u2014 send is being skipped.`);
|
|
16352
16335
|
return;
|
|
16353
16336
|
}
|
|
16354
16337
|
const code = generateOtpCode();
|
|
16355
16338
|
const otpHash = hashOtpCode(code);
|
|
16356
|
-
const otpExpiresAt = new Date(now.getTime() +
|
|
16339
|
+
const otpExpiresAt = new Date(now.getTime() + import_types33.OTP_TTL_MINUTES * 60 * 1e3);
|
|
16357
16340
|
const otpSendCount = withinWindow ? pending.otpSendCount + 1 : 1;
|
|
16358
16341
|
await this.repo.update(pending.id, {
|
|
16359
16342
|
otpHash,
|
|
@@ -16390,7 +16373,7 @@ var PendingRegistrationService = class _PendingRegistrationService {
|
|
|
16390
16373
|
}
|
|
16391
16374
|
const token = await this.resumeTokenSigner.sign({
|
|
16392
16375
|
pendingRegistrationId: pending.id,
|
|
16393
|
-
ttlMinutes:
|
|
16376
|
+
ttlMinutes: import_types32.REGISTRATION_RESUME_TTL_MINUTES
|
|
16394
16377
|
});
|
|
16395
16378
|
const base = (this.resumeBaseUrl ?? "http://localhost").replace(/\/+$/, "");
|
|
16396
16379
|
const url = `${base}/login?resume=${encodeURIComponent(token)}`;
|
|
@@ -16417,28 +16400,20 @@ var PendingRegistrationService = class _PendingRegistrationService {
|
|
|
16417
16400
|
*/
|
|
16418
16401
|
async resumeWithToken(input, context) {
|
|
16419
16402
|
if (!this.resumeTokenSigner) {
|
|
16420
|
-
throw new import_common80.BadRequestException(
|
|
16421
|
-
code: import_types31.REGISTRATION_ERROR_CODES.RESUME_NOT_CONFIGURED
|
|
16422
|
-
});
|
|
16403
|
+
throw new import_common80.BadRequestException(codedError(import_types32.REGISTRATION_ERROR_CODES.RESUME_NOT_CONFIGURED));
|
|
16423
16404
|
}
|
|
16424
16405
|
let decoded;
|
|
16425
16406
|
try {
|
|
16426
16407
|
decoded = await this.resumeTokenSigner.verify(input.token);
|
|
16427
16408
|
} catch {
|
|
16428
|
-
throw new import_common80.BadRequestException(
|
|
16429
|
-
code: import_types31.REGISTRATION_ERROR_CODES.RESUME_TOKEN_INVALID
|
|
16430
|
-
});
|
|
16409
|
+
throw new import_common80.BadRequestException(codedError(import_types32.REGISTRATION_ERROR_CODES.RESUME_TOKEN_INVALID));
|
|
16431
16410
|
}
|
|
16432
16411
|
const pending = await this.repo.findById(decoded.pendingRegistrationId);
|
|
16433
16412
|
if (!pending) {
|
|
16434
|
-
throw new import_common80.BadRequestException(
|
|
16435
|
-
code: import_types31.REGISTRATION_ERROR_CODES.RESUME_TOKEN_INVALID
|
|
16436
|
-
});
|
|
16413
|
+
throw new import_common80.BadRequestException(codedError(import_types32.REGISTRATION_ERROR_CODES.RESUME_TOKEN_INVALID));
|
|
16437
16414
|
}
|
|
16438
16415
|
if (pending.expiresAt.getTime() < Date.now()) {
|
|
16439
|
-
throw new import_common80.BadRequestException(
|
|
16440
|
-
code: import_types31.REGISTRATION_ERROR_CODES.PENDING_REGISTRATION_EXPIRED
|
|
16441
|
-
});
|
|
16416
|
+
throw new import_common80.BadRequestException(codedError(import_types32.REGISTRATION_ERROR_CODES.PENDING_REGISTRATION_EXPIRED));
|
|
16442
16417
|
}
|
|
16443
16418
|
await this.record("REGISTRATION_NEUTRAL_REPLAY", pending.id, context, {
|
|
16444
16419
|
via: "resume_token",
|
|
@@ -16447,7 +16422,7 @@ var PendingRegistrationService = class _PendingRegistrationService {
|
|
|
16447
16422
|
return {
|
|
16448
16423
|
pendingRegistrationId: pending.id,
|
|
16449
16424
|
status: pending.status,
|
|
16450
|
-
nextStep:
|
|
16425
|
+
nextStep: import_types33.REGISTRATION_STEP_BY_STATUS[pending.status],
|
|
16451
16426
|
snapshot: toSnapshot2(pending)
|
|
16452
16427
|
};
|
|
16453
16428
|
}
|
|
@@ -16466,9 +16441,7 @@ var PendingRegistrationService = class _PendingRegistrationService {
|
|
|
16466
16441
|
/** Returns the (app-specific) configurator catalog. */
|
|
16467
16442
|
async getConfiguratorCatalog() {
|
|
16468
16443
|
if (!this.configuratorLookup) {
|
|
16469
|
-
throw new import_common80.BadRequestException(
|
|
16470
|
-
code: import_types31.REGISTRATION_ERROR_CODES.CONFIGURATOR_NOT_CONFIGURED
|
|
16471
|
-
});
|
|
16444
|
+
throw new import_common80.BadRequestException(codedError(import_types32.REGISTRATION_ERROR_CODES.CONFIGURATOR_NOT_CONFIGURED));
|
|
16472
16445
|
}
|
|
16473
16446
|
return this.configuratorLookup.getCatalog();
|
|
16474
16447
|
}
|
|
@@ -16485,32 +16458,22 @@ var PendingRegistrationService = class _PendingRegistrationService {
|
|
|
16485
16458
|
*/
|
|
16486
16459
|
async saveConfiguration(input, context) {
|
|
16487
16460
|
if (!this.configuratorLookup) {
|
|
16488
|
-
throw new import_common80.BadRequestException(
|
|
16489
|
-
code: import_types31.REGISTRATION_ERROR_CODES.CONFIGURATOR_NOT_CONFIGURED
|
|
16490
|
-
});
|
|
16461
|
+
throw new import_common80.BadRequestException(codedError(import_types32.REGISTRATION_ERROR_CODES.CONFIGURATOR_NOT_CONFIGURED));
|
|
16491
16462
|
}
|
|
16492
16463
|
const pending = await this.repo.findById(input.pendingRegistrationId);
|
|
16493
16464
|
if (!pending) {
|
|
16494
|
-
throw new import_common80.NotFoundException(
|
|
16495
|
-
code: import_types31.REGISTRATION_ERROR_CODES.PENDING_REGISTRATION_NOT_FOUND
|
|
16496
|
-
});
|
|
16465
|
+
throw new import_common80.NotFoundException(codedError(import_types32.REGISTRATION_ERROR_CODES.PENDING_REGISTRATION_NOT_FOUND));
|
|
16497
16466
|
}
|
|
16498
16467
|
if (pending.expiresAt.getTime() < Date.now()) {
|
|
16499
|
-
throw new import_common80.BadRequestException(
|
|
16500
|
-
code: import_types31.REGISTRATION_ERROR_CODES.PENDING_REGISTRATION_EXPIRED
|
|
16501
|
-
});
|
|
16468
|
+
throw new import_common80.BadRequestException(codedError(import_types32.REGISTRATION_ERROR_CODES.PENDING_REGISTRATION_EXPIRED));
|
|
16502
16469
|
}
|
|
16503
16470
|
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
|
-
});
|
|
16471
|
+
throw new import_common80.BadRequestException(codedError(import_types32.REGISTRATION_ERROR_CODES.INVALID_REGISTRATION_STATE));
|
|
16507
16472
|
}
|
|
16508
16473
|
const catalog = await this.configuratorLookup.getCatalog();
|
|
16509
16474
|
const model = catalog.models.find((m) => m.id === input.selection.modelId);
|
|
16510
16475
|
if (!model) {
|
|
16511
|
-
throw new import_common80.BadRequestException(
|
|
16512
|
-
code: import_types31.REGISTRATION_ERROR_CODES.MODEL_NOT_AVAILABLE
|
|
16513
|
-
});
|
|
16476
|
+
throw new import_common80.BadRequestException(codedError(import_types32.REGISTRATION_ERROR_CODES.MODEL_NOT_AVAILABLE));
|
|
16514
16477
|
}
|
|
16515
16478
|
let promoEval;
|
|
16516
16479
|
if (input.selection.appliedPromoCode && this.promoPreview) {
|
|
@@ -16542,7 +16505,7 @@ var PendingRegistrationService = class _PendingRegistrationService {
|
|
|
16542
16505
|
};
|
|
16543
16506
|
}
|
|
16544
16507
|
const now = /* @__PURE__ */ new Date();
|
|
16545
|
-
const expiresAt = new Date(now.getTime() +
|
|
16508
|
+
const expiresAt = new Date(now.getTime() + import_types33.PENDING_ONBOARDING_TTL_DAYS * 24 * 60 * 60 * 1e3);
|
|
16546
16509
|
const updated = await this.repo.update(pending.id, {
|
|
16547
16510
|
status: "PLAN_SELECTED",
|
|
16548
16511
|
currentStep: 4,
|
|
@@ -16561,7 +16524,7 @@ var PendingRegistrationService = class _PendingRegistrationService {
|
|
|
16561
16524
|
return {
|
|
16562
16525
|
pendingRegistrationId: updated.id,
|
|
16563
16526
|
status: updated.status,
|
|
16564
|
-
nextStep:
|
|
16527
|
+
nextStep: import_types33.REGISTRATION_STEP_BY_STATUS[updated.status],
|
|
16565
16528
|
selection: input.selection,
|
|
16566
16529
|
breakdown
|
|
16567
16530
|
};
|
|
@@ -16574,28 +16537,20 @@ var PendingRegistrationService = class _PendingRegistrationService {
|
|
|
16574
16537
|
*/
|
|
16575
16538
|
async previewConfigPromo(pendingRegistrationId, code) {
|
|
16576
16539
|
if (!this.configuratorLookup) {
|
|
16577
|
-
throw new import_common80.BadRequestException(
|
|
16578
|
-
code: import_types31.REGISTRATION_ERROR_CODES.CONFIGURATOR_NOT_CONFIGURED
|
|
16579
|
-
});
|
|
16540
|
+
throw new import_common80.BadRequestException(codedError(import_types32.REGISTRATION_ERROR_CODES.CONFIGURATOR_NOT_CONFIGURED));
|
|
16580
16541
|
}
|
|
16581
16542
|
const pending = await this.repo.findById(pendingRegistrationId);
|
|
16582
16543
|
if (!pending) {
|
|
16583
|
-
throw new import_common80.NotFoundException(
|
|
16584
|
-
code: import_types31.REGISTRATION_ERROR_CODES.PENDING_REGISTRATION_NOT_FOUND
|
|
16585
|
-
});
|
|
16544
|
+
throw new import_common80.NotFoundException(codedError(import_types32.REGISTRATION_ERROR_CODES.PENDING_REGISTRATION_NOT_FOUND));
|
|
16586
16545
|
}
|
|
16587
16546
|
const config = pending.configJson;
|
|
16588
16547
|
if (!config) {
|
|
16589
|
-
throw new import_common80.BadRequestException(
|
|
16590
|
-
code: import_types31.REGISTRATION_ERROR_CODES.CONFIG_NOT_SAVED
|
|
16591
|
-
});
|
|
16548
|
+
throw new import_common80.BadRequestException(codedError(import_types32.REGISTRATION_ERROR_CODES.CONFIG_NOT_SAVED));
|
|
16592
16549
|
}
|
|
16593
16550
|
const catalog = await this.configuratorLookup.getCatalog();
|
|
16594
16551
|
const model = catalog.models.find((m) => m.id === config.modelId);
|
|
16595
16552
|
if (!model) {
|
|
16596
|
-
throw new import_common80.BadRequestException(
|
|
16597
|
-
code: import_types31.REGISTRATION_ERROR_CODES.MODEL_NOT_AVAILABLE
|
|
16598
|
-
});
|
|
16553
|
+
throw new import_common80.BadRequestException(codedError(import_types32.REGISTRATION_ERROR_CODES.MODEL_NOT_AVAILABLE));
|
|
16599
16554
|
}
|
|
16600
16555
|
let promoEval;
|
|
16601
16556
|
if (code && this.promoPreview) {
|
|
@@ -16703,9 +16658,9 @@ function normalizeEmail(value) {
|
|
|
16703
16658
|
__name(normalizeEmail, "normalizeEmail");
|
|
16704
16659
|
function resolveOtpVerifyMaxAttempts() {
|
|
16705
16660
|
const raw = process.env.SAAS_PLATFORM_OTP_VERIFY_MAX_ATTEMPTS;
|
|
16706
|
-
if (!raw) return
|
|
16661
|
+
if (!raw) return import_types33.OTP_VERIFY_MAX_ATTEMPTS;
|
|
16707
16662
|
const parsed = Number.parseInt(raw, 10);
|
|
16708
|
-
return Number.isInteger(parsed) && parsed > 0 ? parsed :
|
|
16663
|
+
return Number.isInteger(parsed) && parsed > 0 ? parsed : import_types33.OTP_VERIFY_MAX_ATTEMPTS;
|
|
16709
16664
|
}
|
|
16710
16665
|
__name(resolveOtpVerifyMaxAttempts, "resolveOtpVerifyMaxAttempts");
|
|
16711
16666
|
function toSnapshot2(pending) {
|
|
@@ -16800,7 +16755,7 @@ RegistrationCleanupCron = _ts_decorate91([
|
|
|
16800
16755
|
|
|
16801
16756
|
// src/registration/ip-rate-limit.guard.ts
|
|
16802
16757
|
var import_common82 = require("@nestjs/common");
|
|
16803
|
-
var
|
|
16758
|
+
var import_types34 = require("@saasicat/types");
|
|
16804
16759
|
function _ts_decorate92(decorators, target, key, desc) {
|
|
16805
16760
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
16806
16761
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
@@ -16829,9 +16784,14 @@ var BaseIpRateLimitGuard = class {
|
|
|
16829
16784
|
const key = ipFingerprint2(req);
|
|
16830
16785
|
if (this.exceeded(key)) {
|
|
16831
16786
|
throw new import_common82.HttpException({
|
|
16832
|
-
|
|
16833
|
-
|
|
16834
|
-
|
|
16787
|
+
...codedError(import_types34.REGISTRATION_ERROR_CODES.RATE_LIMITED, {
|
|
16788
|
+
retryAfterSeconds: Math.ceil(this.options.windowMs / 1e3)
|
|
16789
|
+
}),
|
|
16790
|
+
retryAfterSeconds: Math.ceil(this.options.windowMs / 1e3),
|
|
16791
|
+
// Superseded by `code`. Note this carries the limiter name,
|
|
16792
|
+
// not the code — the two emitters of RATE_LIMITED disagree
|
|
16793
|
+
// on what `reason` means, which is why it is going away.
|
|
16794
|
+
reason: this.options.name
|
|
16835
16795
|
}, import_common82.HttpStatus.TOO_MANY_REQUESTS);
|
|
16836
16796
|
}
|
|
16837
16797
|
return true;
|
|
@@ -17580,9 +17540,10 @@ __export(src_exports, {
|
|
|
17580
17540
|
buildPublicMarketingCatalogController: () => buildPublicMarketingCatalogController,
|
|
17581
17541
|
buildReplacedByIndex: () => buildReplacedByIndex,
|
|
17582
17542
|
buildTenantSubscriptionBundlesController: () => buildTenantSubscriptionBundlesController,
|
|
17583
|
-
classifyBundleVersionDiff: () =>
|
|
17584
|
-
classifyPlanDiff: () =>
|
|
17543
|
+
classifyBundleVersionDiff: () => import_types5.classifyBundleVersionDiff,
|
|
17544
|
+
classifyPlanDiff: () => import_types5.classifyPlanDiff,
|
|
17585
17545
|
clearPendingPlanVersionFields: () => clearPendingPlanVersionFields,
|
|
17546
|
+
codedError: () => codedError,
|
|
17586
17547
|
collectSubscriptionBundleFeatures: () => collectSubscriptionBundleFeatures,
|
|
17587
17548
|
computeBreakdown: () => computeBreakdown,
|
|
17588
17549
|
computeCarriedTrialEndsAt: () => computeCarriedTrialEndsAt,
|
|
@@ -17674,7 +17635,7 @@ __export(testing_exports, {
|
|
|
17674
17635
|
});
|
|
17675
17636
|
|
|
17676
17637
|
// src/testing/fake-repositories.ts
|
|
17677
|
-
var
|
|
17638
|
+
var import_types35 = require("@saasicat/types");
|
|
17678
17639
|
var FakeSubscriptionRepository = class {
|
|
17679
17640
|
static {
|
|
17680
17641
|
__name(this, "FakeSubscriptionRepository");
|
|
@@ -18430,7 +18391,7 @@ var FakePlanRepository = class {
|
|
|
18430
18391
|
}
|
|
18431
18392
|
async findActivePlanVersion(planKey, asOf = /* @__PURE__ */ new Date()) {
|
|
18432
18393
|
const t = asOf.getTime();
|
|
18433
|
-
const dayStart = (0,
|
|
18394
|
+
const dayStart = (0, import_types35.startOfUtcDay)(asOf).getTime();
|
|
18434
18395
|
const matches = [
|
|
18435
18396
|
...this.versions.values()
|
|
18436
18397
|
].filter((v) => {
|