@saasicat/nest 0.18.0 → 0.19.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 +1309 -519
- package/dist/admin/index.js +1 -1
- package/dist/billing/index.d.cts +7 -4
- package/dist/billing/index.d.ts +7 -4
- package/dist/billing/index.js +6 -6
- package/dist/catalog/index.js +4 -4
- package/dist/checkout-offer/index.js +2 -2
- package/dist/{chunk-Z3FWN3HJ.js → chunk-5ZXBXYXU.js} +3 -3
- package/dist/{chunk-RZVSKKXX.js → chunk-6WA4X5U3.js} +46 -45
- package/dist/{chunk-V34AA7C7.js → chunk-6WL6VQ7Z.js} +1 -1
- package/dist/{chunk-ABB2CRAS.js → chunk-74SCJ4HF.js} +1 -1
- package/dist/{chunk-PLJEAUFU.js → chunk-7JIHG53V.js} +76 -39
- package/dist/{chunk-7RRWI5LH.js → chunk-AFK2IE2L.js} +10 -10
- package/dist/{chunk-32EMXPU2.js → chunk-B7NRY2LV.js} +48 -17
- package/dist/{chunk-MDIZUVIK.js → chunk-BSK6YBLI.js} +1 -1
- package/dist/{chunk-R2QOSVAA.js → chunk-DQKCK7DX.js} +1 -1
- package/dist/{chunk-GLLCB4BP.js → chunk-EUU3B6NB.js} +17 -13
- package/dist/{chunk-EXOLOJFZ.js → chunk-F6DCLVBB.js} +29 -8
- package/dist/{chunk-5KH2O4SF.js → chunk-I77VS3YH.js} +287 -105
- package/dist/{chunk-YITTE646.js → chunk-KUDKD7ZG.js} +34 -14
- package/dist/{chunk-P3DP6GJE.js → chunk-LXUBCLRK.js} +486 -160
- package/dist/{chunk-GJBXOCRZ.js → chunk-ORWIGSNN.js} +45 -17
- package/dist/{chunk-E7AJ42AB.js → chunk-QLJHIIOY.js} +168 -28
- package/dist/{chunk-BOADBIGB.js → chunk-YYUXKLXK.js} +2 -2
- package/dist/{define-saasicat-Rc0rXrYG.d.ts → define-saasicat-CqkhnHn-.d.ts} +1 -1
- package/dist/{define-saasicat-CeDBX9ji.d.cts → define-saasicat-D8c6Bs4T.d.cts} +1 -1
- package/dist/discovery/index.d.cts +2 -2
- package/dist/discovery/index.d.ts +2 -2
- package/dist/discovery/index.js +4 -4
- package/dist/{enforce-quota.interceptor-DMw-bPyk.d.cts → enforce-quota.interceptor-Dk0eEtzV.d.cts} +1 -1
- package/dist/{enforce-quota.interceptor-CmbWX39L.d.ts → enforce-quota.interceptor-Dp8wTYro.d.ts} +1 -1
- package/dist/entitlement/index.js +2 -2
- package/dist/index.d.cts +6 -6
- package/dist/index.d.ts +6 -6
- package/dist/index.js +17 -17
- package/dist/{module-DgYJur6f.d.ts → module-BwD-wAor.d.ts} +1 -1
- package/dist/{module-CSN8j8Tp.d.cts → module-DsDkbawm.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 +13 -13
- package/dist/promo/index.d.cts +3 -3
- package/dist/promo/index.d.ts +3 -3
- package/dist/promo/index.js +3 -3
- package/dist/registration/index.js +1 -1
- package/dist/{saas-platform.module-DLhoqKNA.d.ts → saas-platform.module-BQmrDGVU.d.ts} +1 -1
- package/dist/{saas-platform.module-D3rUTiho.d.cts → saas-platform.module-DducnQ9s.d.cts} +1 -1
- package/dist/{service-DX8KbGXl.d.cts → service-DPZ2W1E-.d.cts} +2 -2
- package/dist/{service-DX8KbGXl.d.ts → service-DPZ2W1E-.d.ts} +2 -2
- package/dist/subscription-contract/index.js +2 -2
- package/dist/{tenant-billing.controller-BwZRnJ3q.d.ts → tenant-billing.controller-b1G1yB09.d.ts} +2 -2
- package/dist/{tenant-billing.controller-CG4CaFUP.d.cts → tenant-billing.controller-m42YQ_Ei.d.cts} +2 -2
- package/dist/testing/index.d.cts +5 -5
- package/dist/testing/index.d.ts +5 -5
- package/dist/testing/index.js +33 -33
- package/package.json +3 -3
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
} from "./chunk-YSC46BGM.js";
|
|
14
14
|
import {
|
|
15
15
|
DiscoveryScanner
|
|
16
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-5ZXBXYXU.js";
|
|
17
17
|
import {
|
|
18
18
|
SUBSCRIPTION_REPOSITORY_TOKEN
|
|
19
19
|
} from "./chunk-WKVBNTYC.js";
|
|
@@ -78,7 +78,7 @@ function dependencyWarnings(features, snapshot, code) {
|
|
|
78
78
|
if (selected.has(required)) continue;
|
|
79
79
|
warnings.push({
|
|
80
80
|
code,
|
|
81
|
-
message: `Feature '${feature}'
|
|
81
|
+
message: `Feature '${feature}' requires '${required}', which is missing from this selection. The tenant must cover that dependency some other way (for example through the plan) \u2014 otherwise the feature will not work.`,
|
|
82
82
|
field: `features[${index}]`,
|
|
83
83
|
value: required
|
|
84
84
|
});
|
|
@@ -97,7 +97,7 @@ function validateBundleDraft(draft, snapshot, knownPlanKeys = null, marketedOnly
|
|
|
97
97
|
if (approved && !approved.features.has(feature)) {
|
|
98
98
|
warnings.push({
|
|
99
99
|
code: "BUNDLE_FEATURE_NOT_APPROVED",
|
|
100
|
-
message: `Feature '${feature}'
|
|
100
|
+
message: `Feature '${feature}' is not approved (status != approved). Only approved features are sellable \u2014 approve it in the discovery review (#20).`,
|
|
101
101
|
field: `features[${index}]`,
|
|
102
102
|
value: feature
|
|
103
103
|
});
|
|
@@ -106,7 +106,7 @@ function validateBundleDraft(draft, snapshot, knownPlanKeys = null, marketedOnly
|
|
|
106
106
|
}
|
|
107
107
|
warnings.push({
|
|
108
108
|
code: "BUNDLE_FEATURE_UNKNOWN",
|
|
109
|
-
message: `Feature '${feature}'
|
|
109
|
+
message: `Feature '${feature}' was not found in the discovery snapshot. Either the code is missing an @ImplementsCapability(..., { feature: '${feature}' }) annotation, or the feature is obsolete.`,
|
|
110
110
|
field: `features[${index}]`,
|
|
111
111
|
value: feature
|
|
112
112
|
});
|
|
@@ -115,14 +115,14 @@ function validateBundleDraft(draft, snapshot, knownPlanKeys = null, marketedOnly
|
|
|
115
115
|
if (!knownQuotas.has(quotaKey)) {
|
|
116
116
|
warnings.push({
|
|
117
117
|
code: "QUOTA_MISSING",
|
|
118
|
-
message: `Quota '${quotaKey}'
|
|
118
|
+
message: `Quota '${quotaKey}' was not found in the discovery snapshot. In code, a class must be annotated with @DefinesQuota({ key: '${quotaKey}', ... }).`,
|
|
119
119
|
field: `quotas.${quotaKey}`,
|
|
120
120
|
value: quotaKey
|
|
121
121
|
});
|
|
122
122
|
} else if (approved && !approved.quotas.has(quotaKey)) {
|
|
123
123
|
warnings.push({
|
|
124
124
|
code: "QUOTA_NOT_APPROVED",
|
|
125
|
-
message: `Quota '${quotaKey}'
|
|
125
|
+
message: `Quota '${quotaKey}' is not approved (status != approved). Only approved quotas are sellable \u2014 approve it in the discovery review (#20).`,
|
|
126
126
|
field: `quotas.${quotaKey}`,
|
|
127
127
|
value: quotaKey
|
|
128
128
|
});
|
|
@@ -134,7 +134,7 @@ function validateBundleDraft(draft, snapshot, knownPlanKeys = null, marketedOnly
|
|
|
134
134
|
if (knownPlanKeys.has(planKey)) return;
|
|
135
135
|
warnings.push({
|
|
136
136
|
code: "BUNDLE_PLAN_KEY_UNKNOWN",
|
|
137
|
-
message: `Plan '${planKey}'
|
|
137
|
+
message: `Plan '${planKey}' from compatibility.planIds does not exist in the project. Either fix the planKey, or create the plan record first.`,
|
|
138
138
|
field: `compatibility.planIds[${index}]`,
|
|
139
139
|
value: planKey
|
|
140
140
|
});
|
|
@@ -154,7 +154,7 @@ function validatePlanDraft(draft, snapshot, marketedOnlyFeatures = /* @__PURE__
|
|
|
154
154
|
if (approved && !approved.features.has(feature)) {
|
|
155
155
|
warnings.push({
|
|
156
156
|
code: "PLAN_FEATURE_NOT_APPROVED",
|
|
157
|
-
message: `Feature '${feature}'
|
|
157
|
+
message: `Feature '${feature}' is not approved (status != approved). Only approved features are sellable \u2014 approve it in the discovery review (#20).`,
|
|
158
158
|
field: `features[${index}]`,
|
|
159
159
|
value: feature
|
|
160
160
|
});
|
|
@@ -163,7 +163,7 @@ function validatePlanDraft(draft, snapshot, marketedOnlyFeatures = /* @__PURE__
|
|
|
163
163
|
}
|
|
164
164
|
warnings.push({
|
|
165
165
|
code: "PLAN_FEATURE_UNKNOWN",
|
|
166
|
-
message: `Feature '${feature}'
|
|
166
|
+
message: `Feature '${feature}' was not found in the discovery snapshot. Either the code is missing an @ImplementsCapability(..., { feature: '${feature}' }) annotation, or the feature is obsolete.`,
|
|
167
167
|
field: `features[${index}]`,
|
|
168
168
|
value: feature
|
|
169
169
|
});
|
|
@@ -172,14 +172,14 @@ function validatePlanDraft(draft, snapshot, marketedOnlyFeatures = /* @__PURE__
|
|
|
172
172
|
if (!knownQuotas.has(quotaKey)) {
|
|
173
173
|
warnings.push({
|
|
174
174
|
code: "QUOTA_MISSING",
|
|
175
|
-
message: `Quota '${quotaKey}'
|
|
175
|
+
message: `Quota '${quotaKey}' was not found in the discovery snapshot. In code, a class must be annotated with @DefinesQuota({ key: '${quotaKey}', ... }).`,
|
|
176
176
|
field: `quotas.${quotaKey}`,
|
|
177
177
|
value: quotaKey
|
|
178
178
|
});
|
|
179
179
|
} else if (approved && !approved.quotas.has(quotaKey)) {
|
|
180
180
|
warnings.push({
|
|
181
181
|
code: "QUOTA_NOT_APPROVED",
|
|
182
|
-
message: `Quota '${quotaKey}'
|
|
182
|
+
message: `Quota '${quotaKey}' is not approved (status != approved). Only approved quotas are sellable \u2014 approve it in the discovery review (#20).`,
|
|
183
183
|
field: `quotas.${quotaKey}`,
|
|
184
184
|
value: quotaKey
|
|
185
185
|
});
|
|
@@ -192,7 +192,7 @@ __name(validatePlanDraft, "validatePlanDraft");
|
|
|
192
192
|
|
|
193
193
|
// src/catalog/bundles.service.ts
|
|
194
194
|
import { Inject, Injectable, Logger, NotFoundException, Optional, UnprocessableEntityException } from "@nestjs/common";
|
|
195
|
-
import { isVersionEditable } from "@saasicat/types";
|
|
195
|
+
import { CATALOG_ERROR_CODES, isVersionEditable } from "@saasicat/types";
|
|
196
196
|
|
|
197
197
|
// src/core/discovery-snapshot-source.ts
|
|
198
198
|
function resolveDiscoverySnapshot(injected, scanner) {
|
|
@@ -247,7 +247,7 @@ var BundlesService = class _BundlesService {
|
|
|
247
247
|
this.mode = config.strictModeCheckMode ?? "blocking";
|
|
248
248
|
this.marketedOnly = new Set(config.marketedOnlyFeatures ?? []);
|
|
249
249
|
if (this.mode === "blocking" && !hasDiscoverySnapshotSource(this.snapshot, this.scanner)) {
|
|
250
|
-
this.logger.error("BundlesService: strictModeCheckMode=blocking
|
|
250
|
+
this.logger.error("BundlesService: strictModeCheckMode=blocking without a DiscoverySnapshot source \u2014 degrading to warn-only. Wire DiscoveryModule so that enforcement takes effect (#25).");
|
|
251
251
|
this.mode = "warn-only";
|
|
252
252
|
}
|
|
253
253
|
}
|
|
@@ -262,7 +262,15 @@ var BundlesService = class _BundlesService {
|
|
|
262
262
|
}
|
|
263
263
|
async getBundle(bundleId) {
|
|
264
264
|
const bundle = await this.repo.findById(bundleId);
|
|
265
|
-
if (!bundle)
|
|
265
|
+
if (!bundle) {
|
|
266
|
+
throw new NotFoundException({
|
|
267
|
+
code: CATALOG_ERROR_CODES.BUNDLE_NOT_FOUND,
|
|
268
|
+
message: `Bundle '${bundleId}' not found`,
|
|
269
|
+
params: {
|
|
270
|
+
bundleId
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
}
|
|
266
274
|
const versions = await this.annotateEditability(await this.repo.listVersions(bundleId));
|
|
267
275
|
return {
|
|
268
276
|
bundle,
|
|
@@ -272,18 +280,41 @@ var BundlesService = class _BundlesService {
|
|
|
272
280
|
async createBundle(data) {
|
|
273
281
|
const existing = await this.repo.findByKey(data.projectKey, data.bundleKey);
|
|
274
282
|
if (existing) {
|
|
275
|
-
throw new UnprocessableEntityException(
|
|
283
|
+
throw new UnprocessableEntityException({
|
|
284
|
+
code: CATALOG_ERROR_CODES.BUNDLE_ALREADY_EXISTS,
|
|
285
|
+
message: `Bundle '${data.bundleKey}' already exists in project '${data.projectKey}'`,
|
|
286
|
+
params: {
|
|
287
|
+
bundleKey: data.bundleKey,
|
|
288
|
+
projectKey: data.projectKey
|
|
289
|
+
}
|
|
290
|
+
});
|
|
276
291
|
}
|
|
277
292
|
return this.repo.create(data);
|
|
278
293
|
}
|
|
279
294
|
async updateBundle(bundleId, data) {
|
|
280
295
|
const existing = await this.repo.findById(bundleId);
|
|
281
|
-
if (!existing)
|
|
296
|
+
if (!existing) {
|
|
297
|
+
throw new NotFoundException({
|
|
298
|
+
code: CATALOG_ERROR_CODES.BUNDLE_NOT_FOUND,
|
|
299
|
+
message: `Bundle '${bundleId}' not found`,
|
|
300
|
+
params: {
|
|
301
|
+
bundleId
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
}
|
|
282
305
|
return this.repo.update(bundleId, data);
|
|
283
306
|
}
|
|
284
307
|
async softDeleteBundle(bundleId) {
|
|
285
308
|
const existing = await this.repo.findById(bundleId);
|
|
286
|
-
if (!existing)
|
|
309
|
+
if (!existing) {
|
|
310
|
+
throw new NotFoundException({
|
|
311
|
+
code: CATALOG_ERROR_CODES.BUNDLE_NOT_FOUND,
|
|
312
|
+
message: `Bundle '${bundleId}' not found`,
|
|
313
|
+
params: {
|
|
314
|
+
bundleId
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
}
|
|
287
318
|
if (existing.deletedAt !== null) return;
|
|
288
319
|
await this.repo.softDelete(bundleId);
|
|
289
320
|
}
|
|
@@ -295,18 +326,27 @@ var BundlesService = class _BundlesService {
|
|
|
295
326
|
async discardBundleDraft(versionId) {
|
|
296
327
|
const existing = await this.repo.findVersionById(versionId);
|
|
297
328
|
if (!existing) {
|
|
298
|
-
throw new NotFoundException(
|
|
329
|
+
throw new NotFoundException({
|
|
330
|
+
code: CATALOG_ERROR_CODES.BUNDLE_VERSION_NOT_FOUND,
|
|
331
|
+
message: `BundleVersion '${versionId}' not found`,
|
|
332
|
+
params: {
|
|
333
|
+
versionId
|
|
334
|
+
}
|
|
335
|
+
});
|
|
299
336
|
}
|
|
300
337
|
if (existing.publishedAt !== null) {
|
|
301
338
|
throw new UnprocessableEntityException({
|
|
302
|
-
code:
|
|
303
|
-
message: `BundleVersion '${versionId}'
|
|
339
|
+
code: CATALOG_ERROR_CODES.BUNDLE_VERSION_ALREADY_PUBLISHED,
|
|
340
|
+
message: `BundleVersion '${versionId}' is already published and cannot be discarded.`,
|
|
341
|
+
params: {
|
|
342
|
+
versionId
|
|
343
|
+
}
|
|
304
344
|
});
|
|
305
345
|
}
|
|
306
346
|
if (typeof this.repo.deleteDraft !== "function") {
|
|
307
347
|
throw new UnprocessableEntityException({
|
|
308
|
-
code:
|
|
309
|
-
message: "Discard
|
|
348
|
+
code: CATALOG_ERROR_CODES.BUNDLE_VERSION_DISCARD_NOT_IMPLEMENTED,
|
|
349
|
+
message: "Discard is not implemented in the current repository. Implement BundleRepository.deleteDraft."
|
|
310
350
|
});
|
|
311
351
|
}
|
|
312
352
|
await this.repo.deleteDraft(versionId);
|
|
@@ -320,7 +360,13 @@ var BundlesService = class _BundlesService {
|
|
|
320
360
|
async getBundleVersion(versionId) {
|
|
321
361
|
const version = await this.repo.findVersionById(versionId);
|
|
322
362
|
if (!version) {
|
|
323
|
-
throw new NotFoundException(
|
|
363
|
+
throw new NotFoundException({
|
|
364
|
+
code: CATALOG_ERROR_CODES.BUNDLE_VERSION_NOT_FOUND,
|
|
365
|
+
message: `BundleVersion '${versionId}' not found`,
|
|
366
|
+
params: {
|
|
367
|
+
versionId
|
|
368
|
+
}
|
|
369
|
+
});
|
|
324
370
|
}
|
|
325
371
|
const [annotated] = await this.annotateEditability([
|
|
326
372
|
version
|
|
@@ -362,11 +408,24 @@ var BundlesService = class _BundlesService {
|
|
|
362
408
|
async createBundleDraft(data) {
|
|
363
409
|
const bundle = await this.repo.findById(data.bundleId);
|
|
364
410
|
if (!bundle) {
|
|
365
|
-
throw new NotFoundException(
|
|
411
|
+
throw new NotFoundException({
|
|
412
|
+
code: CATALOG_ERROR_CODES.BUNDLE_NOT_FOUND,
|
|
413
|
+
message: `Bundle '${data.bundleId}' not found`,
|
|
414
|
+
params: {
|
|
415
|
+
bundleId: data.bundleId
|
|
416
|
+
}
|
|
417
|
+
});
|
|
366
418
|
}
|
|
367
419
|
const existingDraft = await this.repo.findCurrentDraft(data.bundleId);
|
|
368
420
|
if (existingDraft) {
|
|
369
|
-
throw new UnprocessableEntityException(
|
|
421
|
+
throw new UnprocessableEntityException({
|
|
422
|
+
code: CATALOG_ERROR_CODES.BUNDLE_DRAFT_ALREADY_EXISTS,
|
|
423
|
+
message: `Bundle '${bundle.bundleKey}' already has a draft version v${existingDraft.version}; publish or discard it first`,
|
|
424
|
+
params: {
|
|
425
|
+
bundleKey: bundle.bundleKey,
|
|
426
|
+
draftVersion: existingDraft.version
|
|
427
|
+
}
|
|
428
|
+
});
|
|
370
429
|
}
|
|
371
430
|
if (data.baseVersionId === void 0) {
|
|
372
431
|
const latestLive = await this.repo.findLatestLive(data.bundleId);
|
|
@@ -398,7 +457,13 @@ var BundlesService = class _BundlesService {
|
|
|
398
457
|
async updateBundleDraft(versionId, data) {
|
|
399
458
|
const existing = await this.repo.findVersionById(versionId);
|
|
400
459
|
if (!existing) {
|
|
401
|
-
throw new NotFoundException(
|
|
460
|
+
throw new NotFoundException({
|
|
461
|
+
code: CATALOG_ERROR_CODES.BUNDLE_VERSION_NOT_FOUND,
|
|
462
|
+
message: `BundleVersion '${versionId}' not found`,
|
|
463
|
+
params: {
|
|
464
|
+
versionId
|
|
465
|
+
}
|
|
466
|
+
});
|
|
402
467
|
}
|
|
403
468
|
const [annotated] = await this.annotateEditability([
|
|
404
469
|
existing
|
|
@@ -406,8 +471,11 @@ var BundlesService = class _BundlesService {
|
|
|
406
471
|
const { editable } = isVersionEditable(annotated);
|
|
407
472
|
if (!editable) {
|
|
408
473
|
throw new UnprocessableEntityException({
|
|
409
|
-
code:
|
|
410
|
-
message: `BundleVersion '${versionId}'
|
|
474
|
+
code: CATALOG_ERROR_CODES.BUNDLE_VERSION_NOT_EDITABLE,
|
|
475
|
+
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.`,
|
|
476
|
+
params: {
|
|
477
|
+
versionId
|
|
478
|
+
}
|
|
411
479
|
});
|
|
412
480
|
}
|
|
413
481
|
const merged = {
|
|
@@ -438,10 +506,22 @@ var BundlesService = class _BundlesService {
|
|
|
438
506
|
async publishBundleVersion(versionId, publishMeta) {
|
|
439
507
|
const draft = await this.repo.findVersionById(versionId);
|
|
440
508
|
if (!draft) {
|
|
441
|
-
throw new NotFoundException(
|
|
509
|
+
throw new NotFoundException({
|
|
510
|
+
code: CATALOG_ERROR_CODES.BUNDLE_VERSION_NOT_FOUND,
|
|
511
|
+
message: `BundleVersion '${versionId}' not found`,
|
|
512
|
+
params: {
|
|
513
|
+
versionId
|
|
514
|
+
}
|
|
515
|
+
});
|
|
442
516
|
}
|
|
443
517
|
if (draft.publishedAt !== null) {
|
|
444
|
-
throw new UnprocessableEntityException(
|
|
518
|
+
throw new UnprocessableEntityException({
|
|
519
|
+
code: CATALOG_ERROR_CODES.BUNDLE_VERSION_ALREADY_PUBLISHED,
|
|
520
|
+
message: `BundleVersion '${versionId}' is already published`,
|
|
521
|
+
params: {
|
|
522
|
+
versionId
|
|
523
|
+
}
|
|
524
|
+
});
|
|
445
525
|
}
|
|
446
526
|
const bundle = await this.repo.findById(draft.bundleId);
|
|
447
527
|
const warnings = await this.runStrictCheck({
|
|
@@ -454,8 +534,8 @@ var BundlesService = class _BundlesService {
|
|
|
454
534
|
const explicitZero = /* @__PURE__ */ __name((v) => v !== null && v !== void 0 && Number.parseFloat(String(v)) <= 0, "explicitZero");
|
|
455
535
|
if (explicitZero(draft.monthlyNet) || explicitZero(draft.yearlyNet)) {
|
|
456
536
|
throw new UnprocessableEntityException({
|
|
457
|
-
code:
|
|
458
|
-
message: "
|
|
537
|
+
code: CATALOG_ERROR_CODES.BUNDLE_VERSION_ZERO_PRICE,
|
|
538
|
+
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.",
|
|
459
539
|
monthlyNet: draft.monthlyNet,
|
|
460
540
|
yearlyNet: draft.yearlyNet
|
|
461
541
|
});
|
|
@@ -465,23 +545,30 @@ var BundlesService = class _BundlesService {
|
|
|
465
545
|
const validFromInput = publishMeta.validFrom ?? draft.validFrom;
|
|
466
546
|
if (!validFromInput) {
|
|
467
547
|
throw new UnprocessableEntityException({
|
|
468
|
-
code:
|
|
469
|
-
message: "
|
|
548
|
+
code: CATALOG_ERROR_CODES.BUNDLE_VERSION_VALID_FROM_REQUIRED,
|
|
549
|
+
message: "validFrom must be set when publishing (on the draft or the publish call). SPEC_V2 \xA74.2."
|
|
470
550
|
});
|
|
471
551
|
}
|
|
472
552
|
const validFrom = new Date(validFromInput);
|
|
473
553
|
if (Number.isNaN(validFrom.getTime())) {
|
|
474
554
|
throw new UnprocessableEntityException({
|
|
475
|
-
code:
|
|
476
|
-
message: `validFrom '${validFromInput}'
|
|
555
|
+
code: CATALOG_ERROR_CODES.BUNDLE_VERSION_VALID_FROM_INVALID,
|
|
556
|
+
message: `validFrom '${validFromInput}' is not a valid date`,
|
|
557
|
+
params: {
|
|
558
|
+
validFrom: validFromInput
|
|
559
|
+
}
|
|
477
560
|
});
|
|
478
561
|
}
|
|
479
562
|
if (previous?.validFrom) {
|
|
480
563
|
const prevFrom = new Date(previous.validFrom);
|
|
481
564
|
if (validFrom <= prevFrom) {
|
|
482
565
|
throw new UnprocessableEntityException({
|
|
483
|
-
code:
|
|
484
|
-
message: `validFrom (${validFrom.toISOString()})
|
|
566
|
+
code: CATALOG_ERROR_CODES.BUNDLE_VERSION_VALID_FROM_NOT_AFTER_PREVIOUS,
|
|
567
|
+
message: `validFrom (${validFrom.toISOString()}) must be strictly after the validFrom of the previous version (${previous.validFrom}).`,
|
|
568
|
+
params: {
|
|
569
|
+
validFrom: validFrom.toISOString(),
|
|
570
|
+
previousValidFrom: previous.validFrom
|
|
571
|
+
}
|
|
485
572
|
});
|
|
486
573
|
}
|
|
487
574
|
if (previous.validUntil) {
|
|
@@ -490,8 +577,11 @@ var BundlesService = class _BundlesService {
|
|
|
490
577
|
const requiredStart = new Date(prevUntil.getTime() + dayMs);
|
|
491
578
|
if (validFrom.getTime() !== requiredStart.getTime()) {
|
|
492
579
|
throw new UnprocessableEntityException({
|
|
493
|
-
code:
|
|
494
|
-
message: `
|
|
580
|
+
code: CATALOG_ERROR_CODES.BUNDLE_VERSION_VALID_FROM_NOT_GAPLESS,
|
|
581
|
+
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)}.`,
|
|
582
|
+
params: {
|
|
583
|
+
received: validFrom.toISOString().slice(0, 10)
|
|
584
|
+
},
|
|
495
585
|
requiredValidFrom: requiredStart.toISOString().slice(0, 10),
|
|
496
586
|
previousValidUntil: previous.validUntil
|
|
497
587
|
});
|
|
@@ -502,14 +592,21 @@ var BundlesService = class _BundlesService {
|
|
|
502
592
|
const validUntil = validUntilInput ? new Date(validUntilInput) : null;
|
|
503
593
|
if (validUntil && Number.isNaN(validUntil.getTime())) {
|
|
504
594
|
throw new UnprocessableEntityException({
|
|
505
|
-
code:
|
|
506
|
-
message: `validUntil '${validUntilInput}'
|
|
595
|
+
code: CATALOG_ERROR_CODES.BUNDLE_VERSION_VALID_UNTIL_INVALID,
|
|
596
|
+
message: `validUntil '${validUntilInput}' is not a valid date`,
|
|
597
|
+
params: {
|
|
598
|
+
validUntil: validUntilInput
|
|
599
|
+
}
|
|
507
600
|
});
|
|
508
601
|
}
|
|
509
602
|
if (validUntil && validUntil <= validFrom) {
|
|
510
603
|
throw new UnprocessableEntityException({
|
|
511
|
-
code:
|
|
512
|
-
message: `validUntil (${validUntil.toISOString()})
|
|
604
|
+
code: CATALOG_ERROR_CODES.BUNDLE_VERSION_VALID_UNTIL_BEFORE_FROM,
|
|
605
|
+
message: `validUntil (${validUntil.toISOString()}) must be strictly after validFrom (${validFrom.toISOString()}).`,
|
|
606
|
+
params: {
|
|
607
|
+
validUntil: validUntil.toISOString(),
|
|
608
|
+
validFrom: validFrom.toISOString()
|
|
609
|
+
}
|
|
513
610
|
});
|
|
514
611
|
}
|
|
515
612
|
const diff = previous ? classifyBundleVersionDiff({
|
|
@@ -528,8 +625,8 @@ var BundlesService = class _BundlesService {
|
|
|
528
625
|
};
|
|
529
626
|
if (!diff.nonRegressive && !publishMeta.forceRegressive) {
|
|
530
627
|
throw new UnprocessableEntityException({
|
|
531
|
-
code:
|
|
532
|
-
message: "
|
|
628
|
+
code: CATALOG_ERROR_CODES.BUNDLE_VERSION_REGRESSION,
|
|
629
|
+
message: "This bundle version is regressive (feature removed / quota lowered / price raised). Publishing requires an explicit `forceRegressive: true` (UI confirmation dialog with MFA).",
|
|
533
630
|
changes: diff.changes
|
|
534
631
|
});
|
|
535
632
|
}
|
|
@@ -568,8 +665,8 @@ var BundlesService = class _BundlesService {
|
|
|
568
665
|
const hasHardBlockingWarning = blocking.some((w) => w.code === "BUNDLE_PLAN_KEY_UNKNOWN");
|
|
569
666
|
if (this.mode === "blocking" || hasHardBlockingWarning) {
|
|
570
667
|
throw new UnprocessableEntityException({
|
|
571
|
-
code:
|
|
572
|
-
message: "
|
|
668
|
+
code: CATALOG_ERROR_CODES.STRICT_MODE_VIOLATIONS,
|
|
669
|
+
message: "The strict-mode check found drift against the discovery snapshot.",
|
|
573
670
|
warnings
|
|
574
671
|
});
|
|
575
672
|
}
|
|
@@ -582,8 +679,8 @@ var BundlesService = class _BundlesService {
|
|
|
582
679
|
if (!validFromInput) {
|
|
583
680
|
if (existing.publishedAt !== null) {
|
|
584
681
|
throw new UnprocessableEntityException({
|
|
585
|
-
code:
|
|
586
|
-
message: "Published
|
|
682
|
+
code: CATALOG_ERROR_CODES.BUNDLE_VERSION_VALID_FROM_REQUIRED,
|
|
683
|
+
message: "Published bundle versions must keep a validFrom. Set a new future date, or edit the draft before publishing."
|
|
587
684
|
});
|
|
588
685
|
}
|
|
589
686
|
return;
|
|
@@ -591,14 +688,21 @@ var BundlesService = class _BundlesService {
|
|
|
591
688
|
const validFrom = this.parseRequiredBundleDate(validFromInput, "validFrom", "BUNDLE_VERSION_VALID_FROM_INVALID");
|
|
592
689
|
if (existing.publishedAt !== null && validFrom.getTime() <= Date.now()) {
|
|
593
690
|
throw new UnprocessableEntityException({
|
|
594
|
-
code:
|
|
595
|
-
message: `validFrom (${validFrom.toISOString()})
|
|
691
|
+
code: CATALOG_ERROR_CODES.BUNDLE_VERSION_VALID_FROM_NOT_FUTURE,
|
|
692
|
+
message: `validFrom (${validFrom.toISOString()}) must, for a published-but-future bundle version, still lie in the future.`,
|
|
693
|
+
params: {
|
|
694
|
+
validFrom: validFrom.toISOString()
|
|
695
|
+
}
|
|
596
696
|
});
|
|
597
697
|
}
|
|
598
698
|
if (validUntil && validUntil <= validFrom) {
|
|
599
699
|
throw new UnprocessableEntityException({
|
|
600
|
-
code:
|
|
601
|
-
message: `validUntil (${validUntil.toISOString()})
|
|
700
|
+
code: CATALOG_ERROR_CODES.BUNDLE_VERSION_VALID_UNTIL_BEFORE_FROM,
|
|
701
|
+
message: `validUntil (${validUntil.toISOString()}) must be strictly after validFrom (${validFrom.toISOString()}).`,
|
|
702
|
+
params: {
|
|
703
|
+
validUntil: validUntil.toISOString(),
|
|
704
|
+
validFrom: validFrom.toISOString()
|
|
705
|
+
}
|
|
602
706
|
});
|
|
603
707
|
}
|
|
604
708
|
const previous = await this.findPreviousBundleVersion(existing);
|
|
@@ -607,8 +711,12 @@ var BundlesService = class _BundlesService {
|
|
|
607
711
|
if (Number.isNaN(previousValidFrom.getTime())) return;
|
|
608
712
|
if (validFrom <= previousValidFrom) {
|
|
609
713
|
throw new UnprocessableEntityException({
|
|
610
|
-
code:
|
|
611
|
-
message: `validFrom (${validFrom.toISOString()})
|
|
714
|
+
code: CATALOG_ERROR_CODES.BUNDLE_VERSION_VALID_FROM_NOT_AFTER_PREVIOUS,
|
|
715
|
+
message: `validFrom (${validFrom.toISOString()}) must be strictly after the validFrom of the previous version (${previous.validFrom}).`,
|
|
716
|
+
params: {
|
|
717
|
+
validFrom: validFrom.toISOString(),
|
|
718
|
+
previousValidFrom: previous.validFrom
|
|
719
|
+
}
|
|
612
720
|
});
|
|
613
721
|
}
|
|
614
722
|
if (!previous.validUntil) return;
|
|
@@ -618,8 +726,11 @@ var BundlesService = class _BundlesService {
|
|
|
618
726
|
const requiredStart = new Date(previousValidUntil.getTime() + dayMs);
|
|
619
727
|
if (validFrom.getTime() !== requiredStart.getTime()) {
|
|
620
728
|
throw new UnprocessableEntityException({
|
|
621
|
-
code:
|
|
622
|
-
message: `
|
|
729
|
+
code: CATALOG_ERROR_CODES.BUNDLE_VERSION_VALID_FROM_NOT_GAPLESS,
|
|
730
|
+
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)}.`,
|
|
731
|
+
params: {
|
|
732
|
+
received: validFrom.toISOString().slice(0, 10)
|
|
733
|
+
},
|
|
623
734
|
requiredValidFrom: requiredStart.toISOString().slice(0, 10),
|
|
624
735
|
previousValidUntil: previous.validUntil
|
|
625
736
|
});
|
|
@@ -638,7 +749,11 @@ var BundlesService = class _BundlesService {
|
|
|
638
749
|
if (Number.isNaN(date.getTime())) {
|
|
639
750
|
throw new UnprocessableEntityException({
|
|
640
751
|
code,
|
|
641
|
-
message: `${field} '${input}'
|
|
752
|
+
message: `${field} '${input}' is not a valid date`,
|
|
753
|
+
params: {
|
|
754
|
+
field,
|
|
755
|
+
value: input
|
|
756
|
+
}
|
|
642
757
|
});
|
|
643
758
|
}
|
|
644
759
|
return date;
|
|
@@ -673,6 +788,7 @@ BundlesService = _ts_decorate([
|
|
|
673
788
|
|
|
674
789
|
// src/catalog/marketing-projections.service.ts
|
|
675
790
|
import { ConflictException, Inject as Inject2, Injectable as Injectable2, NotFoundException as NotFoundException2 } from "@nestjs/common";
|
|
791
|
+
import { CATALOG_ERROR_CODES as CATALOG_ERROR_CODES2 } from "@saasicat/types";
|
|
676
792
|
function _ts_decorate2(decorators, target, key, desc) {
|
|
677
793
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
678
794
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
@@ -704,7 +820,13 @@ var MarketingProjectionsService = class {
|
|
|
704
820
|
async getById(id) {
|
|
705
821
|
const row = await this.repo.findById(id);
|
|
706
822
|
if (!row) {
|
|
707
|
-
throw new NotFoundException2(
|
|
823
|
+
throw new NotFoundException2({
|
|
824
|
+
code: CATALOG_ERROR_CODES2.MARKETING_PROJECTION_NOT_FOUND,
|
|
825
|
+
message: `MarketingProjection '${id}' not found`,
|
|
826
|
+
params: {
|
|
827
|
+
projectionId: id
|
|
828
|
+
}
|
|
829
|
+
});
|
|
708
830
|
}
|
|
709
831
|
return row;
|
|
710
832
|
}
|
|
@@ -712,7 +834,15 @@ var MarketingProjectionsService = class {
|
|
|
712
834
|
const locale = data.locale ?? "de";
|
|
713
835
|
const existing = await this.repo.findByTarget(data.targetType, data.targetVersionId, locale);
|
|
714
836
|
if (existing) {
|
|
715
|
-
throw new ConflictException(
|
|
837
|
+
throw new ConflictException({
|
|
838
|
+
code: CATALOG_ERROR_CODES2.MARKETING_PROJECTION_ALREADY_EXISTS,
|
|
839
|
+
message: `Marketing projection for ${data.targetType}/${data.targetVersionId}/${locale} already exists \u2014 use PATCH to edit it`,
|
|
840
|
+
params: {
|
|
841
|
+
targetType: data.targetType,
|
|
842
|
+
targetVersionId: data.targetVersionId,
|
|
843
|
+
locale
|
|
844
|
+
}
|
|
845
|
+
});
|
|
716
846
|
}
|
|
717
847
|
return this.repo.create({
|
|
718
848
|
...data,
|
|
@@ -722,14 +852,26 @@ var MarketingProjectionsService = class {
|
|
|
722
852
|
async update(id, data) {
|
|
723
853
|
const existing = await this.repo.findById(id);
|
|
724
854
|
if (!existing) {
|
|
725
|
-
throw new NotFoundException2(
|
|
855
|
+
throw new NotFoundException2({
|
|
856
|
+
code: CATALOG_ERROR_CODES2.MARKETING_PROJECTION_NOT_FOUND,
|
|
857
|
+
message: `MarketingProjection '${id}' not found`,
|
|
858
|
+
params: {
|
|
859
|
+
projectionId: id
|
|
860
|
+
}
|
|
861
|
+
});
|
|
726
862
|
}
|
|
727
863
|
return this.repo.update(id, data);
|
|
728
864
|
}
|
|
729
865
|
async delete(id) {
|
|
730
866
|
const existing = await this.repo.findById(id);
|
|
731
867
|
if (!existing) {
|
|
732
|
-
throw new NotFoundException2(
|
|
868
|
+
throw new NotFoundException2({
|
|
869
|
+
code: CATALOG_ERROR_CODES2.MARKETING_PROJECTION_NOT_FOUND,
|
|
870
|
+
message: `MarketingProjection '${id}' not found`,
|
|
871
|
+
params: {
|
|
872
|
+
projectionId: id
|
|
873
|
+
}
|
|
874
|
+
});
|
|
733
875
|
}
|
|
734
876
|
await this.repo.delete(id);
|
|
735
877
|
}
|
|
@@ -767,6 +909,7 @@ __name(keyListSegment, "keyListSegment");
|
|
|
767
909
|
|
|
768
910
|
// src/catalog/catalog-entries.service.ts
|
|
769
911
|
import { Inject as Inject3, Injectable as Injectable3, Logger as Logger2, NotFoundException as NotFoundException3, Optional as Optional2, UnprocessableEntityException as UnprocessableEntityException2 } from "@nestjs/common";
|
|
912
|
+
import { CATALOG_ERROR_CODES as CATALOG_ERROR_CODES3 } from "@saasicat/types";
|
|
770
913
|
function _ts_decorate3(decorators, target, key, desc) {
|
|
771
914
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
772
915
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
@@ -855,14 +998,14 @@ var CatalogEntriesService = class _CatalogEntriesService {
|
|
|
855
998
|
if (!this.autoSyncAtBoot) return;
|
|
856
999
|
const snapshot = resolveDiscoverySnapshot(this.snapshot, this.scanner);
|
|
857
1000
|
if (!snapshot) {
|
|
858
|
-
this.logger.warn("Discovery-
|
|
1001
|
+
this.logger.warn("Discovery auto-sync skipped: no snapshot (neither token nor DiscoveryScanner injected). Wire up DiscoveryModule (#25).");
|
|
859
1002
|
return;
|
|
860
1003
|
}
|
|
861
1004
|
try {
|
|
862
1005
|
const result = await this.syncFromSnapshot(snapshot);
|
|
863
1006
|
this.logger.log(`Discovery-Auto-Sync (${snapshot.app.key}): Features ${result.features.discovered}\u2191/${result.features.retired}\u2193/${result.features.outdated}\u26A0, Quotas ${result.quotas.discovered}\u2191/${result.quotas.retired}\u2193/${result.quotas.outdated}\u26A0, Capabilities ${result.capabilities.discovered}\u2191/${result.capabilities.retired}\u2193`);
|
|
864
1007
|
} catch (err) {
|
|
865
|
-
this.logger.error(`Discovery-
|
|
1008
|
+
this.logger.error(`Discovery auto-sync on boot failed (${snapshot.app.key}) \u2014 the catalog may be stale; re-sync manually via POST /admin/catalog/discovery/sync`, err instanceof Error ? err.stack : String(err));
|
|
866
1009
|
}
|
|
867
1010
|
}
|
|
868
1011
|
listCapabilities(projectKey, codeStatus) {
|
|
@@ -892,7 +1035,14 @@ var CatalogEntriesService = class _CatalogEntriesService {
|
|
|
892
1035
|
async reviewFeature(projectKey, featureKey, data, reviewedBy) {
|
|
893
1036
|
const existing = await this.repo.findFeature(projectKey, featureKey);
|
|
894
1037
|
if (!existing) {
|
|
895
|
-
throw new NotFoundException3(
|
|
1038
|
+
throw new NotFoundException3({
|
|
1039
|
+
code: CATALOG_ERROR_CODES3.FEATURE_NOT_FOUND,
|
|
1040
|
+
message: `Feature '${featureKey}' not found in project '${projectKey}'`,
|
|
1041
|
+
params: {
|
|
1042
|
+
featureKey,
|
|
1043
|
+
projectKey
|
|
1044
|
+
}
|
|
1045
|
+
});
|
|
896
1046
|
}
|
|
897
1047
|
return this.repo.setFeatureReview(projectKey, featureKey, this.resolveReviewUpdate(existing, data.discoveryStatus, reviewedBy, (snapshot) => featureApprovalSignature(featureKey, snapshot)));
|
|
898
1048
|
}
|
|
@@ -900,24 +1050,47 @@ var CatalogEntriesService = class _CatalogEntriesService {
|
|
|
900
1050
|
async reviewQuota(projectKey, quotaKey, data, reviewedBy) {
|
|
901
1051
|
const existing = await this.repo.findQuota(projectKey, quotaKey);
|
|
902
1052
|
if (!existing) {
|
|
903
|
-
throw new NotFoundException3(
|
|
1053
|
+
throw new NotFoundException3({
|
|
1054
|
+
code: CATALOG_ERROR_CODES3.QUOTA_NOT_FOUND,
|
|
1055
|
+
message: `Quota '${quotaKey}' not found in project '${projectKey}'`,
|
|
1056
|
+
params: {
|
|
1057
|
+
quotaKey,
|
|
1058
|
+
projectKey
|
|
1059
|
+
}
|
|
1060
|
+
});
|
|
904
1061
|
}
|
|
905
1062
|
return this.repo.setQuotaReview(projectKey, quotaKey, this.resolveReviewUpdate(existing, data.discoveryStatus, reviewedBy, (snapshot) => {
|
|
906
1063
|
const discovered = snapshot.quotas.find((q) => q.quotaKey === quotaKey);
|
|
907
1064
|
if (!discovered) {
|
|
908
|
-
throw new UnprocessableEntityException2(
|
|
1065
|
+
throw new UnprocessableEntityException2({
|
|
1066
|
+
code: CATALOG_ERROR_CODES3.QUOTA_NOT_IN_DISCOVERY_SNAPSHOT,
|
|
1067
|
+
message: `Quota '${quotaKey}' is not in the discovery snapshot \u2014 a quota that is not declared in code cannot be approved`,
|
|
1068
|
+
params: {
|
|
1069
|
+
quotaKey
|
|
1070
|
+
}
|
|
1071
|
+
});
|
|
909
1072
|
}
|
|
910
1073
|
return quotaApprovalSignature(discovered);
|
|
911
1074
|
}));
|
|
912
1075
|
}
|
|
913
1076
|
resolveReviewUpdate(existing, target, reviewedBy, signatureOf) {
|
|
914
1077
|
if (!REVIEW_TRANSITIONS[existing.discoveryStatus].includes(target)) {
|
|
915
|
-
throw new UnprocessableEntityException2(
|
|
1078
|
+
throw new UnprocessableEntityException2({
|
|
1079
|
+
code: CATALOG_ERROR_CODES3.DISCOVERY_STATUS_TRANSITION_INVALID,
|
|
1080
|
+
message: `Transition '${existing.discoveryStatus}' \u2192 '${target}' is not allowed`,
|
|
1081
|
+
params: {
|
|
1082
|
+
from: existing.discoveryStatus,
|
|
1083
|
+
to: target
|
|
1084
|
+
}
|
|
1085
|
+
});
|
|
916
1086
|
}
|
|
917
1087
|
if (target === "approved") {
|
|
918
1088
|
const snapshot = resolveDiscoverySnapshot(this.snapshot, this.scanner);
|
|
919
1089
|
if (!snapshot) {
|
|
920
|
-
throw new UnprocessableEntityException2(
|
|
1090
|
+
throw new UnprocessableEntityException2({
|
|
1091
|
+
code: CATALOG_ERROR_CODES3.DISCOVERY_NOT_INITIALIZED,
|
|
1092
|
+
message: "Approval requires a discovery snapshot \u2014 discovery is not initialised (#25)"
|
|
1093
|
+
});
|
|
921
1094
|
}
|
|
922
1095
|
return {
|
|
923
1096
|
discoveryStatus: "approved",
|
|
@@ -1110,7 +1283,7 @@ var CatalogEntriesService = class _CatalogEntriesService {
|
|
|
1110
1283
|
const claimants = claimantsByOldKey.get(entry.key);
|
|
1111
1284
|
if (claimants && claimants.length > 0) {
|
|
1112
1285
|
if (claimants.length > 1) {
|
|
1113
|
-
this.logger.warn(`${type} '${entry.key}'
|
|
1286
|
+
this.logger.warn(`${type} '${entry.key}' is claimed by several replaces declarations (${claimants.join(", ")}); '${claimants[0]}' wins \u2014 clean up the replaces in the code (#39)`);
|
|
1114
1287
|
}
|
|
1115
1288
|
target = claimants[0];
|
|
1116
1289
|
}
|
|
@@ -1123,7 +1296,7 @@ var CatalogEntriesService = class _CatalogEntriesService {
|
|
|
1123
1296
|
if (changes.length === 0) return 0;
|
|
1124
1297
|
const setSuccessor = type === "feature" ? this.repo.setFeatureSuccessor?.bind(this.repo) : this.repo.setQuotaSuccessor?.bind(this.repo);
|
|
1125
1298
|
if (!setSuccessor) {
|
|
1126
|
-
this.logger.warn(`CatalogEntryRepository
|
|
1299
|
+
this.logger.warn(`CatalogEntryRepository does not implement set${type === "feature" ? "Feature" : "Quota"}Successor \u2014 ${changes.length} successor pointers (#39) are not persisted`);
|
|
1127
1300
|
return 0;
|
|
1128
1301
|
}
|
|
1129
1302
|
let replaced = 0;
|
|
@@ -1194,7 +1367,7 @@ var ListCatalogEntriesQueryDto = class {
|
|
|
1194
1367
|
_ts_decorate4([
|
|
1195
1368
|
IsString(),
|
|
1196
1369
|
Matches(PROJECT_KEY_PATTERN, {
|
|
1197
|
-
message: "projectKey
|
|
1370
|
+
message: "projectKey must be kebab-case"
|
|
1198
1371
|
}),
|
|
1199
1372
|
_ts_metadata4("design:type", String)
|
|
1200
1373
|
], ListCatalogEntriesQueryDto.prototype, "projectKey", void 0);
|
|
@@ -1219,7 +1392,7 @@ var ReviewCatalogEntryDto = class {
|
|
|
1219
1392
|
_ts_decorate4([
|
|
1220
1393
|
IsString(),
|
|
1221
1394
|
IsIn(REVIEW_STATUSES, {
|
|
1222
|
-
message: "discoveryStatus
|
|
1395
|
+
message: "discoveryStatus must be pending|approved|outdated|obsolete"
|
|
1223
1396
|
}),
|
|
1224
1397
|
_ts_metadata4("design:type", typeof DiscoveryStatus === "undefined" ? Object : DiscoveryStatus)
|
|
1225
1398
|
], ReviewCatalogEntryDto.prototype, "discoveryStatus", void 0);
|
|
@@ -1617,6 +1790,7 @@ __name(buildCatalogEntriesController, "buildCatalogEntriesController");
|
|
|
1617
1790
|
|
|
1618
1791
|
// src/catalog/promotions.service.ts
|
|
1619
1792
|
import { Inject as Inject6, Injectable as Injectable5, NotFoundException as NotFoundException4 } from "@nestjs/common";
|
|
1793
|
+
import { CATALOG_ERROR_CODES as CATALOG_ERROR_CODES4 } from "@saasicat/types";
|
|
1620
1794
|
function _ts_decorate7(decorators, target, key, desc) {
|
|
1621
1795
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
1622
1796
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
@@ -1650,7 +1824,13 @@ var PromotionsService = class {
|
|
|
1650
1824
|
async getById(id) {
|
|
1651
1825
|
const row = await this.repo.findById(id);
|
|
1652
1826
|
if (!row) {
|
|
1653
|
-
throw new NotFoundException4(
|
|
1827
|
+
throw new NotFoundException4({
|
|
1828
|
+
code: CATALOG_ERROR_CODES4.PROMOTION_NOT_FOUND,
|
|
1829
|
+
message: `Promotion '${id}' not found`,
|
|
1830
|
+
params: {
|
|
1831
|
+
promotionId: id
|
|
1832
|
+
}
|
|
1833
|
+
});
|
|
1654
1834
|
}
|
|
1655
1835
|
return row;
|
|
1656
1836
|
}
|
|
@@ -1660,14 +1840,26 @@ var PromotionsService = class {
|
|
|
1660
1840
|
async update(id, data) {
|
|
1661
1841
|
const existing = await this.repo.findById(id);
|
|
1662
1842
|
if (!existing) {
|
|
1663
|
-
throw new NotFoundException4(
|
|
1843
|
+
throw new NotFoundException4({
|
|
1844
|
+
code: CATALOG_ERROR_CODES4.PROMOTION_NOT_FOUND,
|
|
1845
|
+
message: `Promotion '${id}' not found`,
|
|
1846
|
+
params: {
|
|
1847
|
+
promotionId: id
|
|
1848
|
+
}
|
|
1849
|
+
});
|
|
1664
1850
|
}
|
|
1665
1851
|
return this.repo.update(id, data);
|
|
1666
1852
|
}
|
|
1667
1853
|
async delete(id) {
|
|
1668
1854
|
const existing = await this.repo.findById(id);
|
|
1669
1855
|
if (!existing) {
|
|
1670
|
-
throw new NotFoundException4(
|
|
1856
|
+
throw new NotFoundException4({
|
|
1857
|
+
code: CATALOG_ERROR_CODES4.PROMOTION_NOT_FOUND,
|
|
1858
|
+
message: `Promotion '${id}' not found`,
|
|
1859
|
+
params: {
|
|
1860
|
+
promotionId: id
|
|
1861
|
+
}
|
|
1862
|
+
});
|
|
1671
1863
|
}
|
|
1672
1864
|
await this.repo.delete(id);
|
|
1673
1865
|
}
|
|
@@ -1721,7 +1913,7 @@ var ListPromotionsQueryDto = class {
|
|
|
1721
1913
|
_ts_decorate8([
|
|
1722
1914
|
IsString2(),
|
|
1723
1915
|
Matches2(PROJECT_KEY_PATTERN2, {
|
|
1724
|
-
message: "projectKey
|
|
1916
|
+
message: "projectKey must be kebab-case"
|
|
1725
1917
|
}),
|
|
1726
1918
|
_ts_metadata8("design:type", String)
|
|
1727
1919
|
], ListPromotionsQueryDto.prototype, "projectKey", void 0);
|
|
@@ -1789,14 +1981,14 @@ _ts_decorate8([
|
|
|
1789
1981
|
_ts_decorate8([
|
|
1790
1982
|
IsString2(),
|
|
1791
1983
|
Matches2(ISO_DATE_PATTERN, {
|
|
1792
|
-
message: "validFrom
|
|
1984
|
+
message: "validFrom must be an ISO date"
|
|
1793
1985
|
}),
|
|
1794
1986
|
_ts_metadata8("design:type", String)
|
|
1795
1987
|
], CreatePromotionDto.prototype, "validFrom", void 0);
|
|
1796
1988
|
_ts_decorate8([
|
|
1797
1989
|
IsString2(),
|
|
1798
1990
|
Matches2(ISO_DATE_PATTERN, {
|
|
1799
|
-
message: "validTo
|
|
1991
|
+
message: "validTo must be an ISO date"
|
|
1800
1992
|
}),
|
|
1801
1993
|
_ts_metadata8("design:type", String)
|
|
1802
1994
|
], CreatePromotionDto.prototype, "validTo", void 0);
|
|
@@ -2115,7 +2307,7 @@ var ListMarketingSettingsQueryDto = class {
|
|
|
2115
2307
|
_ts_decorate11([
|
|
2116
2308
|
IsString3(),
|
|
2117
2309
|
Matches3(PROJECT_KEY_PATTERN3, {
|
|
2118
|
-
message: "projectKey
|
|
2310
|
+
message: "projectKey must be kebab-case"
|
|
2119
2311
|
}),
|
|
2120
2312
|
_ts_metadata11("design:type", String)
|
|
2121
2313
|
], ListMarketingSettingsQueryDto.prototype, "projectKey", void 0);
|
|
@@ -2129,7 +2321,7 @@ var UpdateMarketingSettingsDto = class {
|
|
|
2129
2321
|
_ts_decorate11([
|
|
2130
2322
|
IsString3(),
|
|
2131
2323
|
Matches3(PROJECT_KEY_PATTERN3, {
|
|
2132
|
-
message: "projectKey
|
|
2324
|
+
message: "projectKey must be kebab-case"
|
|
2133
2325
|
}),
|
|
2134
2326
|
_ts_metadata11("design:type", String)
|
|
2135
2327
|
], UpdateMarketingSettingsDto.prototype, "projectKey", void 0);
|
|
@@ -2141,7 +2333,7 @@ _ts_decorate11([
|
|
|
2141
2333
|
}),
|
|
2142
2334
|
Matches3(LOCALE_PATTERN, {
|
|
2143
2335
|
each: true,
|
|
2144
|
-
message: "Locale
|
|
2336
|
+
message: "Locale must be ISO-639-1"
|
|
2145
2337
|
}),
|
|
2146
2338
|
_ts_metadata11("design:type", Array)
|
|
2147
2339
|
], UpdateMarketingSettingsDto.prototype, "activeLocales", void 0);
|
|
@@ -2630,6 +2822,7 @@ __name(parseAsOf, "parseAsOf");
|
|
|
2630
2822
|
|
|
2631
2823
|
// src/catalog/plans.service.ts
|
|
2632
2824
|
import { Inject as Inject12, Injectable as Injectable8, NotFoundException as NotFoundException5, Optional as Optional6, UnprocessableEntityException as UnprocessableEntityException3 } from "@nestjs/common";
|
|
2825
|
+
import { CATALOG_ERROR_CODES as CATALOG_ERROR_CODES5 } from "@saasicat/types";
|
|
2633
2826
|
function _ts_decorate15(decorators, target, key, desc) {
|
|
2634
2827
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
2635
2828
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
@@ -2676,24 +2869,55 @@ var PlansService = class {
|
|
|
2676
2869
|
}
|
|
2677
2870
|
async getPlan(planId) {
|
|
2678
2871
|
const plan = await this.repo.findById(planId);
|
|
2679
|
-
if (!plan)
|
|
2872
|
+
if (!plan) {
|
|
2873
|
+
throw new NotFoundException5({
|
|
2874
|
+
code: CATALOG_ERROR_CODES5.PLAN_NOT_FOUND,
|
|
2875
|
+
message: `Plan '${planId}' not found`,
|
|
2876
|
+
params: {
|
|
2877
|
+
planId
|
|
2878
|
+
}
|
|
2879
|
+
});
|
|
2880
|
+
}
|
|
2680
2881
|
return plan;
|
|
2681
2882
|
}
|
|
2682
2883
|
async createPlan(data) {
|
|
2683
2884
|
const existing = await this.repo.findByKey(data.projectKey, data.planKey);
|
|
2684
2885
|
if (existing) {
|
|
2685
|
-
throw new UnprocessableEntityException3(
|
|
2886
|
+
throw new UnprocessableEntityException3({
|
|
2887
|
+
code: CATALOG_ERROR_CODES5.PLAN_ALREADY_EXISTS,
|
|
2888
|
+
message: `Plan '${data.planKey}' already exists in project '${data.projectKey}'`,
|
|
2889
|
+
params: {
|
|
2890
|
+
planKey: data.planKey,
|
|
2891
|
+
projectKey: data.projectKey
|
|
2892
|
+
}
|
|
2893
|
+
});
|
|
2686
2894
|
}
|
|
2687
2895
|
return this.repo.create(data);
|
|
2688
2896
|
}
|
|
2689
2897
|
async updatePlan(planId, data) {
|
|
2690
2898
|
const existing = await this.repo.findById(planId);
|
|
2691
|
-
if (!existing)
|
|
2899
|
+
if (!existing) {
|
|
2900
|
+
throw new NotFoundException5({
|
|
2901
|
+
code: CATALOG_ERROR_CODES5.PLAN_NOT_FOUND,
|
|
2902
|
+
message: `Plan '${planId}' not found`,
|
|
2903
|
+
params: {
|
|
2904
|
+
planId
|
|
2905
|
+
}
|
|
2906
|
+
});
|
|
2907
|
+
}
|
|
2692
2908
|
return this.repo.update(planId, data);
|
|
2693
2909
|
}
|
|
2694
2910
|
async softDeletePlan(planId) {
|
|
2695
2911
|
const existing = await this.repo.findById(planId);
|
|
2696
|
-
if (!existing)
|
|
2912
|
+
if (!existing) {
|
|
2913
|
+
throw new NotFoundException5({
|
|
2914
|
+
code: CATALOG_ERROR_CODES5.PLAN_NOT_FOUND,
|
|
2915
|
+
message: `Plan '${planId}' not found`,
|
|
2916
|
+
params: {
|
|
2917
|
+
planId
|
|
2918
|
+
}
|
|
2919
|
+
});
|
|
2920
|
+
}
|
|
2697
2921
|
if (existing.deletedAt !== null) return;
|
|
2698
2922
|
await this.assertNoPublishedVersions(existing.planKey, "soft-delete");
|
|
2699
2923
|
await this.repo.softDelete(planId);
|
|
@@ -2710,13 +2934,21 @@ var PlansService = class {
|
|
|
2710
2934
|
*/
|
|
2711
2935
|
async hardDeletePlan(planId) {
|
|
2712
2936
|
const existing = await this.repo.findById(planId);
|
|
2713
|
-
if (!existing)
|
|
2937
|
+
if (!existing) {
|
|
2938
|
+
throw new NotFoundException5({
|
|
2939
|
+
code: CATALOG_ERROR_CODES5.PLAN_NOT_FOUND,
|
|
2940
|
+
message: `Plan '${planId}' not found`,
|
|
2941
|
+
params: {
|
|
2942
|
+
planId
|
|
2943
|
+
}
|
|
2944
|
+
});
|
|
2945
|
+
}
|
|
2714
2946
|
await this.assertNoPublishedVersions(existing.planKey, "hard-delete");
|
|
2715
2947
|
await this.assertNoDrafts(existing.planKey);
|
|
2716
2948
|
if (typeof this.repo.hardDelete !== "function") {
|
|
2717
2949
|
throw new UnprocessableEntityException3({
|
|
2718
|
-
code:
|
|
2719
|
-
message: "
|
|
2950
|
+
code: CATALOG_ERROR_CODES5.PLAN_HARD_DELETE_NOT_IMPLEMENTED,
|
|
2951
|
+
message: "Hard delete is not implemented in the current repository. Implement PlanRepository.hardDelete."
|
|
2720
2952
|
});
|
|
2721
2953
|
}
|
|
2722
2954
|
await this.repo.hardDelete(planId);
|
|
@@ -2736,8 +2968,12 @@ var PlansService = class {
|
|
|
2736
2968
|
const live = published.filter((v) => v.supersededAt === null).length;
|
|
2737
2969
|
const superseded = published.length - live;
|
|
2738
2970
|
throw new UnprocessableEntityException3({
|
|
2739
|
-
code:
|
|
2740
|
-
message: `Plan '${planKey}'
|
|
2971
|
+
code: CATALOG_ERROR_CODES5.PLAN_HAS_PUBLISHED_VERSIONS,
|
|
2972
|
+
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.`,
|
|
2973
|
+
params: {
|
|
2974
|
+
planKey,
|
|
2975
|
+
operation: op
|
|
2976
|
+
},
|
|
2741
2977
|
publishedCount: published.length,
|
|
2742
2978
|
liveCount: live,
|
|
2743
2979
|
supersededCount: superseded
|
|
@@ -2749,8 +2985,11 @@ var PlansService = class {
|
|
|
2749
2985
|
const drafts = versions.filter((v) => v.publishedAt === null);
|
|
2750
2986
|
if (drafts.length === 0) return;
|
|
2751
2987
|
throw new UnprocessableEntityException3({
|
|
2752
|
-
code:
|
|
2753
|
-
message: `Plan '${planKey}'
|
|
2988
|
+
code: CATALOG_ERROR_CODES5.PLAN_HAS_DRAFTS,
|
|
2989
|
+
message: `Plan '${planKey}' still has ${drafts.length} open draft version(s). Discard them first (DELETE /admin/catalog/plan-versions/:id) or publish them.`,
|
|
2990
|
+
params: {
|
|
2991
|
+
planKey
|
|
2992
|
+
},
|
|
2754
2993
|
draftCount: drafts.length
|
|
2755
2994
|
});
|
|
2756
2995
|
}
|
|
@@ -2769,7 +3008,7 @@ PlansService = _ts_decorate15([
|
|
|
2769
3008
|
|
|
2770
3009
|
// src/catalog/plan-versions.service.ts
|
|
2771
3010
|
import { Inject as Inject13, Injectable as Injectable9, Logger as Logger3, NotFoundException as NotFoundException6, Optional as Optional7, UnprocessableEntityException as UnprocessableEntityException4 } from "@nestjs/common";
|
|
2772
|
-
import { isVersionEditable as isVersionEditable2 } from "@saasicat/types";
|
|
3011
|
+
import { CATALOG_ERROR_CODES as CATALOG_ERROR_CODES6, isVersionEditable as isVersionEditable2 } from "@saasicat/types";
|
|
2773
3012
|
function _ts_decorate16(decorators, target, key, desc) {
|
|
2774
3013
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
2775
3014
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
@@ -2808,7 +3047,7 @@ var PlanVersionsService = class _PlanVersionsService {
|
|
|
2808
3047
|
this.mode = config.strictModeCheckMode ?? "blocking";
|
|
2809
3048
|
this.marketedOnly = new Set(config.marketedOnlyFeatures ?? []);
|
|
2810
3049
|
if (this.mode === "blocking" && !hasDiscoverySnapshotSource(this.snapshot, this.scanner)) {
|
|
2811
|
-
this.logger.error("PlanVersionsService: strictModeCheckMode=blocking
|
|
3050
|
+
this.logger.error("PlanVersionsService: strictModeCheckMode=blocking without a DiscoverySnapshot source \u2014 degrading to warn-only. Wire DiscoveryModule (#25).");
|
|
2812
3051
|
this.mode = "warn-only";
|
|
2813
3052
|
}
|
|
2814
3053
|
const required = [
|
|
@@ -2822,7 +3061,7 @@ var PlanVersionsService = class _PlanVersionsService {
|
|
|
2822
3061
|
];
|
|
2823
3062
|
for (const key of required) {
|
|
2824
3063
|
if (typeof this.repo[key] !== "function") {
|
|
2825
|
-
throw new Error(`PlanVersionsService: PlanRepository.${String(key)}
|
|
3064
|
+
throw new Error(`PlanVersionsService: PlanRepository.${String(key)} is missing \u2014 the repository does not implement the lifecycle methods.`);
|
|
2826
3065
|
}
|
|
2827
3066
|
}
|
|
2828
3067
|
}
|
|
@@ -2838,18 +3077,27 @@ var PlanVersionsService = class _PlanVersionsService {
|
|
|
2838
3077
|
async discardPlanDraft(versionId) {
|
|
2839
3078
|
const existing = await this.repo.findVersionById(versionId);
|
|
2840
3079
|
if (!existing) {
|
|
2841
|
-
throw new NotFoundException6(
|
|
3080
|
+
throw new NotFoundException6({
|
|
3081
|
+
code: CATALOG_ERROR_CODES6.PLAN_VERSION_NOT_FOUND,
|
|
3082
|
+
message: `PlanVersion '${versionId}' not found`,
|
|
3083
|
+
params: {
|
|
3084
|
+
versionId
|
|
3085
|
+
}
|
|
3086
|
+
});
|
|
2842
3087
|
}
|
|
2843
3088
|
if (existing.publishedAt !== null) {
|
|
2844
3089
|
throw new UnprocessableEntityException4({
|
|
2845
|
-
code:
|
|
2846
|
-
message: `PlanVersion '${versionId}'
|
|
3090
|
+
code: CATALOG_ERROR_CODES6.PLAN_VERSION_ALREADY_PUBLISHED,
|
|
3091
|
+
message: `PlanVersion '${versionId}' is already published and cannot be discarded.`,
|
|
3092
|
+
params: {
|
|
3093
|
+
versionId
|
|
3094
|
+
}
|
|
2847
3095
|
});
|
|
2848
3096
|
}
|
|
2849
3097
|
if (typeof this.repo.deletePlanVersionDraft !== "function") {
|
|
2850
3098
|
throw new UnprocessableEntityException4({
|
|
2851
|
-
code:
|
|
2852
|
-
message: "Discard
|
|
3099
|
+
code: CATALOG_ERROR_CODES6.PLAN_VERSION_DISCARD_NOT_IMPLEMENTED,
|
|
3100
|
+
message: "Discard is not implemented in the current repository. Implement PlanRepository.deletePlanVersionDraft."
|
|
2853
3101
|
});
|
|
2854
3102
|
}
|
|
2855
3103
|
await this.repo.deletePlanVersionDraft(versionId);
|
|
@@ -2862,7 +3110,13 @@ var PlanVersionsService = class _PlanVersionsService {
|
|
|
2862
3110
|
async listPlanVersions(planUuid) {
|
|
2863
3111
|
const plan = await this.repo.findById(planUuid);
|
|
2864
3112
|
if (!plan) {
|
|
2865
|
-
throw new NotFoundException6(
|
|
3113
|
+
throw new NotFoundException6({
|
|
3114
|
+
code: CATALOG_ERROR_CODES6.PLAN_NOT_FOUND,
|
|
3115
|
+
message: `Plan '${planUuid}' not found`,
|
|
3116
|
+
params: {
|
|
3117
|
+
planId: planUuid
|
|
3118
|
+
}
|
|
3119
|
+
});
|
|
2866
3120
|
}
|
|
2867
3121
|
const versions = await this.repo.listVersions(plan.planKey);
|
|
2868
3122
|
return this.annotateEditability(versions);
|
|
@@ -2870,7 +3124,13 @@ var PlanVersionsService = class _PlanVersionsService {
|
|
|
2870
3124
|
async getPlanVersion(versionId) {
|
|
2871
3125
|
const version = await this.repo.findVersionById(versionId);
|
|
2872
3126
|
if (!version) {
|
|
2873
|
-
throw new NotFoundException6(
|
|
3127
|
+
throw new NotFoundException6({
|
|
3128
|
+
code: CATALOG_ERROR_CODES6.PLAN_VERSION_NOT_FOUND,
|
|
3129
|
+
message: `PlanVersion '${versionId}' not found`,
|
|
3130
|
+
params: {
|
|
3131
|
+
versionId
|
|
3132
|
+
}
|
|
3133
|
+
});
|
|
2874
3134
|
}
|
|
2875
3135
|
const [annotated] = await this.annotateEditability([
|
|
2876
3136
|
version
|
|
@@ -2908,12 +3168,25 @@ var PlanVersionsService = class _PlanVersionsService {
|
|
|
2908
3168
|
async createPlanDraft(data) {
|
|
2909
3169
|
const plan = await this.repo.findById(data.planId);
|
|
2910
3170
|
if (!plan) {
|
|
2911
|
-
throw new NotFoundException6(
|
|
3171
|
+
throw new NotFoundException6({
|
|
3172
|
+
code: CATALOG_ERROR_CODES6.PLAN_NOT_FOUND,
|
|
3173
|
+
message: `Plan '${data.planId}' not found`,
|
|
3174
|
+
params: {
|
|
3175
|
+
planId: data.planId
|
|
3176
|
+
}
|
|
3177
|
+
});
|
|
2912
3178
|
}
|
|
2913
3179
|
const planKey = plan.planKey;
|
|
2914
3180
|
const existingDraft = await this.repo.findCurrentDraft(planKey);
|
|
2915
3181
|
if (existingDraft) {
|
|
2916
|
-
throw new UnprocessableEntityException4(
|
|
3182
|
+
throw new UnprocessableEntityException4({
|
|
3183
|
+
code: CATALOG_ERROR_CODES6.PLAN_DRAFT_ALREADY_EXISTS,
|
|
3184
|
+
message: `Plan '${planKey}' already has a draft version v${existingDraft.version}; publish or discard it first`,
|
|
3185
|
+
params: {
|
|
3186
|
+
planKey,
|
|
3187
|
+
draftVersion: existingDraft.version
|
|
3188
|
+
}
|
|
3189
|
+
});
|
|
2917
3190
|
}
|
|
2918
3191
|
let baseVersionId = data.baseVersionId;
|
|
2919
3192
|
if (baseVersionId === void 0) {
|
|
@@ -2938,7 +3211,13 @@ var PlanVersionsService = class _PlanVersionsService {
|
|
|
2938
3211
|
async updatePlanDraft(versionId, data) {
|
|
2939
3212
|
const existing = await this.repo.findVersionById(versionId);
|
|
2940
3213
|
if (!existing) {
|
|
2941
|
-
throw new NotFoundException6(
|
|
3214
|
+
throw new NotFoundException6({
|
|
3215
|
+
code: CATALOG_ERROR_CODES6.PLAN_VERSION_NOT_FOUND,
|
|
3216
|
+
message: `PlanVersion '${versionId}' not found`,
|
|
3217
|
+
params: {
|
|
3218
|
+
versionId
|
|
3219
|
+
}
|
|
3220
|
+
});
|
|
2942
3221
|
}
|
|
2943
3222
|
const [annotated] = await this.annotateEditability([
|
|
2944
3223
|
existing
|
|
@@ -2946,8 +3225,11 @@ var PlanVersionsService = class _PlanVersionsService {
|
|
|
2946
3225
|
const { editable } = isVersionEditable2(annotated);
|
|
2947
3226
|
if (!editable) {
|
|
2948
3227
|
throw new UnprocessableEntityException4({
|
|
2949
|
-
code:
|
|
2950
|
-
message: `PlanVersion '${versionId}'
|
|
3228
|
+
code: CATALOG_ERROR_CODES6.PLAN_VERSION_NOT_EDITABLE,
|
|
3229
|
+
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.`,
|
|
3230
|
+
params: {
|
|
3231
|
+
versionId
|
|
3232
|
+
}
|
|
2951
3233
|
});
|
|
2952
3234
|
}
|
|
2953
3235
|
const merged = {
|
|
@@ -2965,10 +3247,22 @@ var PlanVersionsService = class _PlanVersionsService {
|
|
|
2965
3247
|
async publishPlanVersion(versionId, publishMeta) {
|
|
2966
3248
|
const draft = await this.repo.findVersionById(versionId);
|
|
2967
3249
|
if (!draft) {
|
|
2968
|
-
throw new NotFoundException6(
|
|
3250
|
+
throw new NotFoundException6({
|
|
3251
|
+
code: CATALOG_ERROR_CODES6.PLAN_VERSION_NOT_FOUND,
|
|
3252
|
+
message: `PlanVersion '${versionId}' not found`,
|
|
3253
|
+
params: {
|
|
3254
|
+
versionId
|
|
3255
|
+
}
|
|
3256
|
+
});
|
|
2969
3257
|
}
|
|
2970
3258
|
if (draft.publishedAt !== null) {
|
|
2971
|
-
throw new UnprocessableEntityException4(
|
|
3259
|
+
throw new UnprocessableEntityException4({
|
|
3260
|
+
code: CATALOG_ERROR_CODES6.PLAN_VERSION_ALREADY_PUBLISHED,
|
|
3261
|
+
message: `PlanVersion '${versionId}' is already published`,
|
|
3262
|
+
params: {
|
|
3263
|
+
versionId
|
|
3264
|
+
}
|
|
3265
|
+
});
|
|
2972
3266
|
}
|
|
2973
3267
|
const warnings = await this.runStrictCheck({
|
|
2974
3268
|
features: draft.features,
|
|
@@ -2980,8 +3274,8 @@ var PlanVersionsService = class _PlanVersionsService {
|
|
|
2980
3274
|
const yearly = Number.parseFloat(String(draft.yearlyNet ?? "0"));
|
|
2981
3275
|
if (monthly <= 0 || yearly <= 0) {
|
|
2982
3276
|
throw new UnprocessableEntityException4({
|
|
2983
|
-
code:
|
|
2984
|
-
message: "
|
|
3277
|
+
code: CATALOG_ERROR_CODES6.PLAN_VERSION_ZERO_PRICE,
|
|
3278
|
+
message: "A plan version cannot be published with a price of 0.00 (guard against seed placeholder). For deliberately free special contracts, set allowZeroPrice.",
|
|
2985
3279
|
monthlyNet: String(draft.monthlyNet),
|
|
2986
3280
|
yearlyNet: String(draft.yearlyNet)
|
|
2987
3281
|
});
|
|
@@ -2991,23 +3285,30 @@ var PlanVersionsService = class _PlanVersionsService {
|
|
|
2991
3285
|
const validFromInput = publishMeta.validFrom ?? draft.validFrom;
|
|
2992
3286
|
if (!validFromInput) {
|
|
2993
3287
|
throw new UnprocessableEntityException4({
|
|
2994
|
-
code:
|
|
2995
|
-
message: "
|
|
3288
|
+
code: CATALOG_ERROR_CODES6.PLAN_VERSION_VALID_FROM_REQUIRED,
|
|
3289
|
+
message: "validFrom must be set when publishing (on the draft or the publish call). SPEC_V2 \xA74.2."
|
|
2996
3290
|
});
|
|
2997
3291
|
}
|
|
2998
3292
|
const validFrom = new Date(validFromInput);
|
|
2999
3293
|
if (Number.isNaN(validFrom.getTime())) {
|
|
3000
3294
|
throw new UnprocessableEntityException4({
|
|
3001
|
-
code:
|
|
3002
|
-
message: `validFrom '${validFromInput}'
|
|
3295
|
+
code: CATALOG_ERROR_CODES6.PLAN_VERSION_VALID_FROM_INVALID,
|
|
3296
|
+
message: `validFrom '${validFromInput}' is not a valid date`,
|
|
3297
|
+
params: {
|
|
3298
|
+
validFrom: validFromInput
|
|
3299
|
+
}
|
|
3003
3300
|
});
|
|
3004
3301
|
}
|
|
3005
3302
|
if (previous?.validFrom) {
|
|
3006
3303
|
const prevFrom = new Date(previous.validFrom);
|
|
3007
3304
|
if (validFrom <= prevFrom) {
|
|
3008
3305
|
throw new UnprocessableEntityException4({
|
|
3009
|
-
code:
|
|
3010
|
-
message: `validFrom (${validFrom.toISOString()})
|
|
3306
|
+
code: CATALOG_ERROR_CODES6.PLAN_VERSION_VALID_FROM_NOT_AFTER_PREVIOUS,
|
|
3307
|
+
message: `validFrom (${validFrom.toISOString()}) must be strictly after the validFrom of the previous version (${previous.validFrom}).`,
|
|
3308
|
+
params: {
|
|
3309
|
+
validFrom: validFrom.toISOString(),
|
|
3310
|
+
previousValidFrom: previous.validFrom
|
|
3311
|
+
}
|
|
3011
3312
|
});
|
|
3012
3313
|
}
|
|
3013
3314
|
if (previous.validUntil) {
|
|
@@ -3016,8 +3317,11 @@ var PlanVersionsService = class _PlanVersionsService {
|
|
|
3016
3317
|
const requiredStart = new Date(prevUntil.getTime() + dayMs);
|
|
3017
3318
|
if (validFrom.getTime() !== requiredStart.getTime()) {
|
|
3018
3319
|
throw new UnprocessableEntityException4({
|
|
3019
|
-
code:
|
|
3020
|
-
message: `
|
|
3320
|
+
code: CATALOG_ERROR_CODES6.PLAN_VERSION_VALID_FROM_NOT_GAPLESS,
|
|
3321
|
+
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)}.`,
|
|
3322
|
+
params: {
|
|
3323
|
+
received: validFrom.toISOString().slice(0, 10)
|
|
3324
|
+
},
|
|
3021
3325
|
requiredValidFrom: requiredStart.toISOString().slice(0, 10),
|
|
3022
3326
|
previousValidUntil: previous.validUntil
|
|
3023
3327
|
});
|
|
@@ -3028,14 +3332,21 @@ var PlanVersionsService = class _PlanVersionsService {
|
|
|
3028
3332
|
const validUntil = validUntilInput ? new Date(validUntilInput) : null;
|
|
3029
3333
|
if (validUntil && Number.isNaN(validUntil.getTime())) {
|
|
3030
3334
|
throw new UnprocessableEntityException4({
|
|
3031
|
-
code:
|
|
3032
|
-
message: `validUntil '${validUntilInput}'
|
|
3335
|
+
code: CATALOG_ERROR_CODES6.PLAN_VERSION_VALID_UNTIL_INVALID,
|
|
3336
|
+
message: `validUntil '${validUntilInput}' is not a valid date`,
|
|
3337
|
+
params: {
|
|
3338
|
+
validUntil: validUntilInput
|
|
3339
|
+
}
|
|
3033
3340
|
});
|
|
3034
3341
|
}
|
|
3035
3342
|
if (validUntil && validUntil <= validFrom) {
|
|
3036
3343
|
throw new UnprocessableEntityException4({
|
|
3037
|
-
code:
|
|
3038
|
-
message: `validUntil (${validUntil.toISOString()})
|
|
3344
|
+
code: CATALOG_ERROR_CODES6.PLAN_VERSION_VALID_UNTIL_BEFORE_FROM,
|
|
3345
|
+
message: `validUntil (${validUntil.toISOString()}) must be strictly after validFrom (${validFrom.toISOString()}).`,
|
|
3346
|
+
params: {
|
|
3347
|
+
validUntil: validUntil.toISOString(),
|
|
3348
|
+
validFrom: validFrom.toISOString()
|
|
3349
|
+
}
|
|
3039
3350
|
});
|
|
3040
3351
|
}
|
|
3041
3352
|
const fieldsFor = /* @__PURE__ */ __name((v) => ({
|
|
@@ -3052,8 +3363,8 @@ var PlanVersionsService = class _PlanVersionsService {
|
|
|
3052
3363
|
};
|
|
3053
3364
|
if (!diff.nonRegressive && !publishMeta.forceRegressive) {
|
|
3054
3365
|
throw new UnprocessableEntityException4({
|
|
3055
|
-
code:
|
|
3056
|
-
message: "
|
|
3366
|
+
code: CATALOG_ERROR_CODES6.PLAN_VERSION_REGRESSION,
|
|
3367
|
+
message: "This plan version is regressive (feature removed / quota lowered / price raised). Publishing requires an explicit `forceRegressive: true` (UI confirmation dialog with MFA).",
|
|
3057
3368
|
changes: diff.changes
|
|
3058
3369
|
});
|
|
3059
3370
|
}
|
|
@@ -3088,36 +3399,51 @@ var PlanVersionsService = class _PlanVersionsService {
|
|
|
3088
3399
|
async terminatePlanVersion(versionId, endsAt) {
|
|
3089
3400
|
const existing = await this.repo.findVersionById(versionId);
|
|
3090
3401
|
if (!existing) {
|
|
3091
|
-
throw new NotFoundException6(
|
|
3402
|
+
throw new NotFoundException6({
|
|
3403
|
+
code: CATALOG_ERROR_CODES6.PLAN_VERSION_NOT_FOUND,
|
|
3404
|
+
message: `PlanVersion '${versionId}' not found`,
|
|
3405
|
+
params: {
|
|
3406
|
+
versionId
|
|
3407
|
+
}
|
|
3408
|
+
});
|
|
3092
3409
|
}
|
|
3093
3410
|
if (existing.publishedAt === null) {
|
|
3094
3411
|
throw new UnprocessableEntityException4({
|
|
3095
|
-
code:
|
|
3096
|
-
message: `PlanVersion '${versionId}'
|
|
3412
|
+
code: CATALOG_ERROR_CODES6.PLAN_VERSION_NOT_LIVE,
|
|
3413
|
+
message: `PlanVersion '${versionId}' is not published and cannot be terminated.`,
|
|
3414
|
+
params: {
|
|
3415
|
+
versionId
|
|
3416
|
+
}
|
|
3097
3417
|
});
|
|
3098
3418
|
}
|
|
3099
3419
|
if (existing.supersededAt !== null) {
|
|
3100
3420
|
throw new UnprocessableEntityException4({
|
|
3101
|
-
code:
|
|
3102
|
-
message: `PlanVersion '${versionId}'
|
|
3421
|
+
code: CATALOG_ERROR_CODES6.PLAN_VERSION_NOT_LIVE,
|
|
3422
|
+
message: `PlanVersion '${versionId}' has already been superseded by a newer version (supersededAt) and cannot be terminated.`,
|
|
3423
|
+
params: {
|
|
3424
|
+
versionId
|
|
3425
|
+
}
|
|
3103
3426
|
});
|
|
3104
3427
|
}
|
|
3105
3428
|
if (Number.isNaN(endsAt.getTime())) {
|
|
3106
3429
|
throw new UnprocessableEntityException4({
|
|
3107
|
-
code:
|
|
3108
|
-
message: "endsAt
|
|
3430
|
+
code: CATALOG_ERROR_CODES6.PLAN_TERMINATE_INVALID_DATE,
|
|
3431
|
+
message: "endsAt is not a valid date."
|
|
3109
3432
|
});
|
|
3110
3433
|
}
|
|
3111
3434
|
if (endsAt.getTime() <= Date.now()) {
|
|
3112
3435
|
throw new UnprocessableEntityException4({
|
|
3113
|
-
code:
|
|
3114
|
-
message: `endsAt (${endsAt.toISOString()})
|
|
3436
|
+
code: CATALOG_ERROR_CODES6.PLAN_TERMINATE_DATE_NOT_FUTURE,
|
|
3437
|
+
message: `endsAt (${endsAt.toISOString()}) must lie strictly in the future.`,
|
|
3438
|
+
params: {
|
|
3439
|
+
endsAt: endsAt.toISOString()
|
|
3440
|
+
}
|
|
3115
3441
|
});
|
|
3116
3442
|
}
|
|
3117
3443
|
if (typeof this.repo.terminate !== "function") {
|
|
3118
3444
|
throw new UnprocessableEntityException4({
|
|
3119
|
-
code:
|
|
3120
|
-
message: "Terminate
|
|
3445
|
+
code: CATALOG_ERROR_CODES6.PLAN_TERMINATE_NOT_IMPLEMENTED,
|
|
3446
|
+
message: "Terminate is not implemented in the current repository. Implement PlanRepository.terminate."
|
|
3121
3447
|
});
|
|
3122
3448
|
}
|
|
3123
3449
|
return this.repo.terminate(versionId, endsAt);
|
|
@@ -3135,8 +3461,8 @@ var PlanVersionsService = class _PlanVersionsService {
|
|
|
3135
3461
|
if (blockingStrictModeWarnings(warnings).length === 0) return;
|
|
3136
3462
|
if (this.mode === "blocking") {
|
|
3137
3463
|
throw new UnprocessableEntityException4({
|
|
3138
|
-
code:
|
|
3139
|
-
message: "
|
|
3464
|
+
code: CATALOG_ERROR_CODES6.STRICT_MODE_VIOLATIONS,
|
|
3465
|
+
message: "The strict-mode check found drift against the discovery snapshot.",
|
|
3140
3466
|
warnings
|
|
3141
3467
|
});
|
|
3142
3468
|
}
|
|
@@ -3200,7 +3526,7 @@ var CreateBundleDto = class {
|
|
|
3200
3526
|
_ts_decorate17([
|
|
3201
3527
|
IsString4(),
|
|
3202
3528
|
Matches4(PROJECT_KEY_PATTERN4, {
|
|
3203
|
-
message: 'projectKey
|
|
3529
|
+
message: 'projectKey must be kebab-case (e.g. "my-app")'
|
|
3204
3530
|
}),
|
|
3205
3531
|
MaxLength3(64),
|
|
3206
3532
|
_ts_metadata17("design:type", String)
|
|
@@ -3208,7 +3534,7 @@ _ts_decorate17([
|
|
|
3208
3534
|
_ts_decorate17([
|
|
3209
3535
|
IsString4(),
|
|
3210
3536
|
Matches4(KEY_PATTERN, {
|
|
3211
|
-
message: 'bundleKey
|
|
3537
|
+
message: 'bundleKey must be SCREAMING_SNAKE_CASE (e.g. "BANKING")'
|
|
3212
3538
|
}),
|
|
3213
3539
|
MaxLength3(64),
|
|
3214
3540
|
_ts_metadata17("design:type", String)
|
|
@@ -3308,7 +3634,7 @@ _ts_decorate17([
|
|
|
3308
3634
|
}),
|
|
3309
3635
|
Matches4(FEATURE_KEY_PATTERN, {
|
|
3310
3636
|
each: true,
|
|
3311
|
-
message: "features
|
|
3637
|
+
message: "features entries must be SCREAMING_SNAKE_CASE"
|
|
3312
3638
|
}),
|
|
3313
3639
|
_ts_metadata17("design:type", Array)
|
|
3314
3640
|
], CreateBundleVersionDraftDto.prototype, "features", void 0);
|
|
@@ -3334,7 +3660,7 @@ _ts_decorate17([
|
|
|
3334
3660
|
no_symbols: false
|
|
3335
3661
|
}),
|
|
3336
3662
|
Matches4(DECIMAL_PATTERN, {
|
|
3337
|
-
message: 'monthlyNet
|
|
3663
|
+
message: 'monthlyNet must be a decimal with at most 2 fraction digits (e.g. "9.90")'
|
|
3338
3664
|
}),
|
|
3339
3665
|
_ts_metadata17("design:type", Object)
|
|
3340
3666
|
], CreateBundleVersionDraftDto.prototype, "monthlyNet", void 0);
|
|
@@ -3345,7 +3671,7 @@ _ts_decorate17([
|
|
|
3345
3671
|
no_symbols: false
|
|
3346
3672
|
}),
|
|
3347
3673
|
Matches4(DECIMAL_PATTERN, {
|
|
3348
|
-
message: "yearlyNet
|
|
3674
|
+
message: "yearlyNet must be a decimal with at most 2 fraction digits"
|
|
3349
3675
|
}),
|
|
3350
3676
|
_ts_metadata17("design:type", Object)
|
|
3351
3677
|
], CreateBundleVersionDraftDto.prototype, "yearlyNet", void 0);
|
|
@@ -3369,7 +3695,7 @@ _ts_decorate17([
|
|
|
3369
3695
|
IsOptional3(),
|
|
3370
3696
|
ValidateIf((_o, value) => value !== null),
|
|
3371
3697
|
Matches4(ISO_DATE_PATTERN2, {
|
|
3372
|
-
message: "validFrom
|
|
3698
|
+
message: "validFrom must be an ISO date (YYYY-MM-DD)"
|
|
3373
3699
|
}),
|
|
3374
3700
|
_ts_metadata17("design:type", Object)
|
|
3375
3701
|
], CreateBundleVersionDraftDto.prototype, "validFrom", void 0);
|
|
@@ -3377,7 +3703,7 @@ _ts_decorate17([
|
|
|
3377
3703
|
IsOptional3(),
|
|
3378
3704
|
ValidateIf((_o, value) => value !== null),
|
|
3379
3705
|
Matches4(ISO_DATE_PATTERN2, {
|
|
3380
|
-
message: "validUntil
|
|
3706
|
+
message: "validUntil must be an ISO date (YYYY-MM-DD)"
|
|
3381
3707
|
}),
|
|
3382
3708
|
_ts_metadata17("design:type", Object)
|
|
3383
3709
|
], CreateBundleVersionDraftDto.prototype, "validUntil", void 0);
|
|
@@ -3449,7 +3775,7 @@ _ts_decorate17([
|
|
|
3449
3775
|
IsOptional3(),
|
|
3450
3776
|
ValidateIf((_o, value) => value !== null),
|
|
3451
3777
|
Matches4(ISO_DATE_PATTERN2, {
|
|
3452
|
-
message: "validFrom
|
|
3778
|
+
message: "validFrom must be an ISO date (YYYY-MM-DD)"
|
|
3453
3779
|
}),
|
|
3454
3780
|
_ts_metadata17("design:type", Object)
|
|
3455
3781
|
], UpdateBundleVersionDraftDto.prototype, "validFrom", void 0);
|
|
@@ -3457,7 +3783,7 @@ _ts_decorate17([
|
|
|
3457
3783
|
IsOptional3(),
|
|
3458
3784
|
ValidateIf((_o, value) => value !== null),
|
|
3459
3785
|
Matches4(ISO_DATE_PATTERN2, {
|
|
3460
|
-
message: "validUntil
|
|
3786
|
+
message: "validUntil must be an ISO date (YYYY-MM-DD)"
|
|
3461
3787
|
}),
|
|
3462
3788
|
_ts_metadata17("design:type", Object)
|
|
3463
3789
|
], UpdateBundleVersionDraftDto.prototype, "validUntil", void 0);
|
|
@@ -3494,7 +3820,7 @@ _ts_decorate17([
|
|
|
3494
3820
|
IsOptional3(),
|
|
3495
3821
|
ValidateIf((_o, value) => value !== null),
|
|
3496
3822
|
Matches4(ISO_DATE_PATTERN2, {
|
|
3497
|
-
message: "validFrom
|
|
3823
|
+
message: "validFrom must be an ISO date (YYYY-MM-DD)"
|
|
3498
3824
|
}),
|
|
3499
3825
|
_ts_metadata17("design:type", Object)
|
|
3500
3826
|
], PublishBundleVersionDto.prototype, "validFrom", void 0);
|
|
@@ -3502,7 +3828,7 @@ _ts_decorate17([
|
|
|
3502
3828
|
IsOptional3(),
|
|
3503
3829
|
ValidateIf((_o, value) => value !== null),
|
|
3504
3830
|
Matches4(ISO_DATE_PATTERN2, {
|
|
3505
|
-
message: "validUntil
|
|
3831
|
+
message: "validUntil must be an ISO date (YYYY-MM-DD)"
|
|
3506
3832
|
}),
|
|
3507
3833
|
_ts_metadata17("design:type", Object)
|
|
3508
3834
|
], PublishBundleVersionDto.prototype, "validUntil", void 0);
|
|
@@ -3804,7 +4130,7 @@ var CreateMarketingProjectionDto = class {
|
|
|
3804
4130
|
_ts_decorate19([
|
|
3805
4131
|
IsString5(),
|
|
3806
4132
|
Matches5(PROJECT_KEY_PATTERN5, {
|
|
3807
|
-
message: "projectKey
|
|
4133
|
+
message: "projectKey must be kebab-case"
|
|
3808
4134
|
}),
|
|
3809
4135
|
MaxLength4(64),
|
|
3810
4136
|
_ts_metadata19("design:type", String)
|
|
@@ -3822,7 +4148,7 @@ _ts_decorate19([
|
|
|
3822
4148
|
IsOptional4(),
|
|
3823
4149
|
IsString5(),
|
|
3824
4150
|
Matches5(LOCALE_PATTERN2, {
|
|
3825
|
-
message: 'locale
|
|
4151
|
+
message: 'locale must be ISO-639-1 (e.g. "de", "en", "de-AT")'
|
|
3826
4152
|
}),
|
|
3827
4153
|
_ts_metadata19("design:type", String)
|
|
3828
4154
|
], CreateMarketingProjectionDto.prototype, "locale", void 0);
|
|
@@ -4151,7 +4477,7 @@ var CreatePlanDto = class {
|
|
|
4151
4477
|
_ts_decorate21([
|
|
4152
4478
|
IsString6(),
|
|
4153
4479
|
Matches6(PROJECT_KEY_PATTERN6, {
|
|
4154
|
-
message: 'projectKey
|
|
4480
|
+
message: 'projectKey must be kebab-case (e.g. "my-app")'
|
|
4155
4481
|
}),
|
|
4156
4482
|
MaxLength5(64),
|
|
4157
4483
|
_ts_metadata21("design:type", String)
|
|
@@ -4159,7 +4485,7 @@ _ts_decorate21([
|
|
|
4159
4485
|
_ts_decorate21([
|
|
4160
4486
|
IsString6(),
|
|
4161
4487
|
Matches6(KEY_PATTERN2, {
|
|
4162
|
-
message: 'planKey
|
|
4488
|
+
message: 'planKey must be SCREAMING_SNAKE_CASE (e.g. "STARTER")'
|
|
4163
4489
|
}),
|
|
4164
4490
|
MaxLength5(64),
|
|
4165
4491
|
_ts_metadata21("design:type", String)
|
|
@@ -4268,7 +4594,7 @@ _ts_decorate22([
|
|
|
4268
4594
|
}),
|
|
4269
4595
|
Matches7(FEATURE_KEY_PATTERN2, {
|
|
4270
4596
|
each: true,
|
|
4271
|
-
message: "features
|
|
4597
|
+
message: "features entries must be SCREAMING_SNAKE_CASE"
|
|
4272
4598
|
}),
|
|
4273
4599
|
_ts_metadata22("design:type", Array)
|
|
4274
4600
|
], CreatePlanVersionDraftDto.prototype, "features", void 0);
|
|
@@ -4280,7 +4606,7 @@ _ts_decorate22([
|
|
|
4280
4606
|
}),
|
|
4281
4607
|
Matches7(FEATURE_KEY_PATTERN2, {
|
|
4282
4608
|
each: true,
|
|
4283
|
-
message: "bundles
|
|
4609
|
+
message: "bundles entries must be SCREAMING_SNAKE_CASE"
|
|
4284
4610
|
}),
|
|
4285
4611
|
_ts_metadata22("design:type", Array)
|
|
4286
4612
|
], CreatePlanVersionDraftDto.prototype, "bundles", void 0);
|
|
@@ -4290,13 +4616,13 @@ _ts_decorate22([
|
|
|
4290
4616
|
], CreatePlanVersionDraftDto.prototype, "quotas", void 0);
|
|
4291
4617
|
_ts_decorate22([
|
|
4292
4618
|
Matches7(DECIMAL_PATTERN2, {
|
|
4293
|
-
message: 'monthlyNet
|
|
4619
|
+
message: 'monthlyNet must be a decimal with at most 2 fraction digits (e.g. "9.90")'
|
|
4294
4620
|
}),
|
|
4295
4621
|
_ts_metadata22("design:type", String)
|
|
4296
4622
|
], CreatePlanVersionDraftDto.prototype, "monthlyNet", void 0);
|
|
4297
4623
|
_ts_decorate22([
|
|
4298
4624
|
Matches7(DECIMAL_PATTERN2, {
|
|
4299
|
-
message: "yearlyNet
|
|
4625
|
+
message: "yearlyNet must be a decimal with at most 2 fraction digits"
|
|
4300
4626
|
}),
|
|
4301
4627
|
_ts_metadata22("design:type", String)
|
|
4302
4628
|
], CreatePlanVersionDraftDto.prototype, "yearlyNet", void 0);
|
|
@@ -4541,7 +4867,7 @@ function buildPlansController(guards) {
|
|
|
4541
4867
|
}
|
|
4542
4868
|
assertVersionsService() {
|
|
4543
4869
|
if (!this.versionsService) {
|
|
4544
|
-
throw new Error("PlanVersionsService
|
|
4870
|
+
throw new Error("PlanVersionsService is not registered \u2014 the app has not enabled the plan lifecycle editor.");
|
|
4545
4871
|
}
|
|
4546
4872
|
}
|
|
4547
4873
|
};
|