@saasicat/nest 0.18.1 → 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 +1341 -590
- package/dist/admin/index.js +2 -1
- package/dist/billing/index.d.cts +10 -6
- package/dist/billing/index.d.ts +10 -6
- package/dist/billing/index.js +7 -6
- package/dist/catalog/index.js +4 -4
- package/dist/checkout-offer/index.js +2 -2
- package/dist/{chunk-RZVSKKXX.js → chunk-3DRZ5GCP.js} +56 -105
- package/dist/{chunk-GLLCB4BP.js → chunk-4O6R6ARQ.js} +23 -13
- package/dist/{chunk-7RRWI5LH.js → chunk-55QGAZRQ.js} +10 -10
- package/dist/{chunk-Z3FWN3HJ.js → chunk-5ZXBXYXU.js} +3 -3
- package/dist/{chunk-V34AA7C7.js → chunk-6WL6VQ7Z.js} +1 -1
- package/dist/{chunk-ABB2CRAS.js → chunk-74SCJ4HF.js} +1 -1
- package/dist/{chunk-EXOLOJFZ.js → chunk-AGOG5B77.js} +36 -9
- 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-EBJJNTW6.js +22 -0
- package/dist/{chunk-PLJEAUFU.js → chunk-FYWLPYBK.js} +79 -39
- package/dist/{chunk-P3DP6GJE.js → chunk-GC5AKN44.js} +487 -160
- package/dist/{chunk-5KH2O4SF.js → chunk-NZLAYCEX.js} +286 -110
- package/dist/{chunk-E7AJ42AB.js → chunk-VQQWF7MW.js} +172 -28
- package/dist/{chunk-GJBXOCRZ.js → chunk-XTVAFJ3P.js} +46 -17
- package/dist/{chunk-BOADBIGB.js → chunk-YYUXKLXK.js} +2 -2
- package/dist/{chunk-YITTE646.js → chunk-ZH5PYEMI.js} +41 -23
- package/dist/{define-saasicat-Rc0rXrYG.d.ts → define-saasicat-B20Ms8UC.d.ts} +1 -1
- package/dist/{define-saasicat-CeDBX9ji.d.cts → define-saasicat-Cf9DpEsY.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-CmbWX39L.d.ts → enforce-quota.interceptor-CUyfR4cQ.d.ts} +1 -1
- package/dist/{enforce-quota.interceptor-DMw-bPyk.d.cts → enforce-quota.interceptor-Dtg5WdFj.d.cts} +1 -1
- package/dist/entitlement/index.js +3 -2
- package/dist/index.d.cts +27 -8
- package/dist/index.d.ts +27 -8
- package/dist/index.js +21 -17
- package/dist/{module-DgYJur6f.d.ts → module-BE17XFLg.d.ts} +1 -1
- package/dist/{module-CSN8j8Tp.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 +14 -13
- package/dist/promo/index.d.cts +3 -3
- package/dist/promo/index.d.ts +3 -3
- package/dist/promo/index.js +4 -3
- package/dist/registration/index.js +2 -1
- package/dist/{saas-platform.module-D3rUTiho.d.cts → saas-platform.module-BfaYfL-G.d.cts} +1 -1
- package/dist/{saas-platform.module-DLhoqKNA.d.ts → saas-platform.module-DChhDrR-.d.ts} +1 -1
- package/dist/{service-DX8KbGXl.d.cts → service-D48clObR.d.cts} +3 -3
- package/dist/{service-DX8KbGXl.d.ts → service-D48clObR.d.ts} +3 -3
- package/dist/subscription-contract/index.js +2 -2
- package/dist/{tenant-billing.controller-CG4CaFUP.d.cts → tenant-billing.controller-BIF-mstX.d.cts} +2 -2
- package/dist/{tenant-billing.controller-BwZRnJ3q.d.ts → tenant-billing.controller-DT5UrnDX.d.ts} +2 -2
- package/dist/testing/index.d.cts +5 -5
- package/dist/testing/index.d.ts +5 -5
- package/dist/testing/index.js +34 -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
|
+
bundleVersionId: 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
|
+
bundleVersionId: 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
|
+
bundleVersionId: 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
|
+
bundleVersionId: 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,12 @@ 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
|
+
// Keyed by the field name so these params match the publish
|
|
754
|
+
// path, where the same codes carry `validFrom` / `validUntil`.
|
|
755
|
+
params: {
|
|
756
|
+
[field]: input
|
|
757
|
+
}
|
|
642
758
|
});
|
|
643
759
|
}
|
|
644
760
|
return date;
|
|
@@ -673,6 +789,7 @@ BundlesService = _ts_decorate([
|
|
|
673
789
|
|
|
674
790
|
// src/catalog/marketing-projections.service.ts
|
|
675
791
|
import { ConflictException, Inject as Inject2, Injectable as Injectable2, NotFoundException as NotFoundException2 } from "@nestjs/common";
|
|
792
|
+
import { CATALOG_ERROR_CODES as CATALOG_ERROR_CODES2 } from "@saasicat/types";
|
|
676
793
|
function _ts_decorate2(decorators, target, key, desc) {
|
|
677
794
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
678
795
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
@@ -704,7 +821,13 @@ var MarketingProjectionsService = class {
|
|
|
704
821
|
async getById(id) {
|
|
705
822
|
const row = await this.repo.findById(id);
|
|
706
823
|
if (!row) {
|
|
707
|
-
throw new NotFoundException2(
|
|
824
|
+
throw new NotFoundException2({
|
|
825
|
+
code: CATALOG_ERROR_CODES2.MARKETING_PROJECTION_NOT_FOUND,
|
|
826
|
+
message: `MarketingProjection '${id}' not found`,
|
|
827
|
+
params: {
|
|
828
|
+
projectionId: id
|
|
829
|
+
}
|
|
830
|
+
});
|
|
708
831
|
}
|
|
709
832
|
return row;
|
|
710
833
|
}
|
|
@@ -712,7 +835,15 @@ var MarketingProjectionsService = class {
|
|
|
712
835
|
const locale = data.locale ?? "de";
|
|
713
836
|
const existing = await this.repo.findByTarget(data.targetType, data.targetVersionId, locale);
|
|
714
837
|
if (existing) {
|
|
715
|
-
throw new ConflictException(
|
|
838
|
+
throw new ConflictException({
|
|
839
|
+
code: CATALOG_ERROR_CODES2.MARKETING_PROJECTION_ALREADY_EXISTS,
|
|
840
|
+
message: `Marketing projection for ${data.targetType}/${data.targetVersionId}/${locale} already exists \u2014 use PATCH to edit it`,
|
|
841
|
+
params: {
|
|
842
|
+
targetType: data.targetType,
|
|
843
|
+
targetVersionId: data.targetVersionId,
|
|
844
|
+
locale
|
|
845
|
+
}
|
|
846
|
+
});
|
|
716
847
|
}
|
|
717
848
|
return this.repo.create({
|
|
718
849
|
...data,
|
|
@@ -722,14 +853,26 @@ var MarketingProjectionsService = class {
|
|
|
722
853
|
async update(id, data) {
|
|
723
854
|
const existing = await this.repo.findById(id);
|
|
724
855
|
if (!existing) {
|
|
725
|
-
throw new NotFoundException2(
|
|
856
|
+
throw new NotFoundException2({
|
|
857
|
+
code: CATALOG_ERROR_CODES2.MARKETING_PROJECTION_NOT_FOUND,
|
|
858
|
+
message: `MarketingProjection '${id}' not found`,
|
|
859
|
+
params: {
|
|
860
|
+
projectionId: id
|
|
861
|
+
}
|
|
862
|
+
});
|
|
726
863
|
}
|
|
727
864
|
return this.repo.update(id, data);
|
|
728
865
|
}
|
|
729
866
|
async delete(id) {
|
|
730
867
|
const existing = await this.repo.findById(id);
|
|
731
868
|
if (!existing) {
|
|
732
|
-
throw new NotFoundException2(
|
|
869
|
+
throw new NotFoundException2({
|
|
870
|
+
code: CATALOG_ERROR_CODES2.MARKETING_PROJECTION_NOT_FOUND,
|
|
871
|
+
message: `MarketingProjection '${id}' not found`,
|
|
872
|
+
params: {
|
|
873
|
+
projectionId: id
|
|
874
|
+
}
|
|
875
|
+
});
|
|
733
876
|
}
|
|
734
877
|
await this.repo.delete(id);
|
|
735
878
|
}
|
|
@@ -767,6 +910,7 @@ __name(keyListSegment, "keyListSegment");
|
|
|
767
910
|
|
|
768
911
|
// src/catalog/catalog-entries.service.ts
|
|
769
912
|
import { Inject as Inject3, Injectable as Injectable3, Logger as Logger2, NotFoundException as NotFoundException3, Optional as Optional2, UnprocessableEntityException as UnprocessableEntityException2 } from "@nestjs/common";
|
|
913
|
+
import { CATALOG_ERROR_CODES as CATALOG_ERROR_CODES3 } from "@saasicat/types";
|
|
770
914
|
function _ts_decorate3(decorators, target, key, desc) {
|
|
771
915
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
772
916
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
@@ -855,14 +999,14 @@ var CatalogEntriesService = class _CatalogEntriesService {
|
|
|
855
999
|
if (!this.autoSyncAtBoot) return;
|
|
856
1000
|
const snapshot = resolveDiscoverySnapshot(this.snapshot, this.scanner);
|
|
857
1001
|
if (!snapshot) {
|
|
858
|
-
this.logger.warn("Discovery-
|
|
1002
|
+
this.logger.warn("Discovery auto-sync skipped: no snapshot (neither token nor DiscoveryScanner injected). Wire up DiscoveryModule (#25).");
|
|
859
1003
|
return;
|
|
860
1004
|
}
|
|
861
1005
|
try {
|
|
862
1006
|
const result = await this.syncFromSnapshot(snapshot);
|
|
863
1007
|
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
1008
|
} catch (err) {
|
|
865
|
-
this.logger.error(`Discovery-
|
|
1009
|
+
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
1010
|
}
|
|
867
1011
|
}
|
|
868
1012
|
listCapabilities(projectKey, codeStatus) {
|
|
@@ -892,7 +1036,14 @@ var CatalogEntriesService = class _CatalogEntriesService {
|
|
|
892
1036
|
async reviewFeature(projectKey, featureKey, data, reviewedBy) {
|
|
893
1037
|
const existing = await this.repo.findFeature(projectKey, featureKey);
|
|
894
1038
|
if (!existing) {
|
|
895
|
-
throw new NotFoundException3(
|
|
1039
|
+
throw new NotFoundException3({
|
|
1040
|
+
code: CATALOG_ERROR_CODES3.FEATURE_NOT_FOUND,
|
|
1041
|
+
message: `Feature '${featureKey}' not found in project '${projectKey}'`,
|
|
1042
|
+
params: {
|
|
1043
|
+
featureKey,
|
|
1044
|
+
projectKey
|
|
1045
|
+
}
|
|
1046
|
+
});
|
|
896
1047
|
}
|
|
897
1048
|
return this.repo.setFeatureReview(projectKey, featureKey, this.resolveReviewUpdate(existing, data.discoveryStatus, reviewedBy, (snapshot) => featureApprovalSignature(featureKey, snapshot)));
|
|
898
1049
|
}
|
|
@@ -900,24 +1051,47 @@ var CatalogEntriesService = class _CatalogEntriesService {
|
|
|
900
1051
|
async reviewQuota(projectKey, quotaKey, data, reviewedBy) {
|
|
901
1052
|
const existing = await this.repo.findQuota(projectKey, quotaKey);
|
|
902
1053
|
if (!existing) {
|
|
903
|
-
throw new NotFoundException3(
|
|
1054
|
+
throw new NotFoundException3({
|
|
1055
|
+
code: CATALOG_ERROR_CODES3.QUOTA_NOT_FOUND,
|
|
1056
|
+
message: `Quota '${quotaKey}' not found in project '${projectKey}'`,
|
|
1057
|
+
params: {
|
|
1058
|
+
quotaKey,
|
|
1059
|
+
projectKey
|
|
1060
|
+
}
|
|
1061
|
+
});
|
|
904
1062
|
}
|
|
905
1063
|
return this.repo.setQuotaReview(projectKey, quotaKey, this.resolveReviewUpdate(existing, data.discoveryStatus, reviewedBy, (snapshot) => {
|
|
906
1064
|
const discovered = snapshot.quotas.find((q) => q.quotaKey === quotaKey);
|
|
907
1065
|
if (!discovered) {
|
|
908
|
-
throw new UnprocessableEntityException2(
|
|
1066
|
+
throw new UnprocessableEntityException2({
|
|
1067
|
+
code: CATALOG_ERROR_CODES3.QUOTA_NOT_IN_DISCOVERY_SNAPSHOT,
|
|
1068
|
+
message: `Quota '${quotaKey}' is not in the discovery snapshot \u2014 a quota that is not declared in code cannot be approved`,
|
|
1069
|
+
params: {
|
|
1070
|
+
quotaKey
|
|
1071
|
+
}
|
|
1072
|
+
});
|
|
909
1073
|
}
|
|
910
1074
|
return quotaApprovalSignature(discovered);
|
|
911
1075
|
}));
|
|
912
1076
|
}
|
|
913
1077
|
resolveReviewUpdate(existing, target, reviewedBy, signatureOf) {
|
|
914
1078
|
if (!REVIEW_TRANSITIONS[existing.discoveryStatus].includes(target)) {
|
|
915
|
-
throw new UnprocessableEntityException2(
|
|
1079
|
+
throw new UnprocessableEntityException2({
|
|
1080
|
+
code: CATALOG_ERROR_CODES3.DISCOVERY_STATUS_TRANSITION_INVALID,
|
|
1081
|
+
message: `Transition '${existing.discoveryStatus}' \u2192 '${target}' is not allowed`,
|
|
1082
|
+
params: {
|
|
1083
|
+
from: existing.discoveryStatus,
|
|
1084
|
+
to: target
|
|
1085
|
+
}
|
|
1086
|
+
});
|
|
916
1087
|
}
|
|
917
1088
|
if (target === "approved") {
|
|
918
1089
|
const snapshot = resolveDiscoverySnapshot(this.snapshot, this.scanner);
|
|
919
1090
|
if (!snapshot) {
|
|
920
|
-
throw new UnprocessableEntityException2(
|
|
1091
|
+
throw new UnprocessableEntityException2({
|
|
1092
|
+
code: CATALOG_ERROR_CODES3.DISCOVERY_NOT_INITIALIZED,
|
|
1093
|
+
message: "Approval requires a discovery snapshot \u2014 discovery is not initialised (#25)"
|
|
1094
|
+
});
|
|
921
1095
|
}
|
|
922
1096
|
return {
|
|
923
1097
|
discoveryStatus: "approved",
|
|
@@ -1110,7 +1284,7 @@ var CatalogEntriesService = class _CatalogEntriesService {
|
|
|
1110
1284
|
const claimants = claimantsByOldKey.get(entry.key);
|
|
1111
1285
|
if (claimants && claimants.length > 0) {
|
|
1112
1286
|
if (claimants.length > 1) {
|
|
1113
|
-
this.logger.warn(`${type} '${entry.key}'
|
|
1287
|
+
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
1288
|
}
|
|
1115
1289
|
target = claimants[0];
|
|
1116
1290
|
}
|
|
@@ -1123,7 +1297,7 @@ var CatalogEntriesService = class _CatalogEntriesService {
|
|
|
1123
1297
|
if (changes.length === 0) return 0;
|
|
1124
1298
|
const setSuccessor = type === "feature" ? this.repo.setFeatureSuccessor?.bind(this.repo) : this.repo.setQuotaSuccessor?.bind(this.repo);
|
|
1125
1299
|
if (!setSuccessor) {
|
|
1126
|
-
this.logger.warn(`CatalogEntryRepository
|
|
1300
|
+
this.logger.warn(`CatalogEntryRepository does not implement set${type === "feature" ? "Feature" : "Quota"}Successor \u2014 ${changes.length} successor pointers (#39) are not persisted`);
|
|
1127
1301
|
return 0;
|
|
1128
1302
|
}
|
|
1129
1303
|
let replaced = 0;
|
|
@@ -1194,7 +1368,7 @@ var ListCatalogEntriesQueryDto = class {
|
|
|
1194
1368
|
_ts_decorate4([
|
|
1195
1369
|
IsString(),
|
|
1196
1370
|
Matches(PROJECT_KEY_PATTERN, {
|
|
1197
|
-
message: "projectKey
|
|
1371
|
+
message: "projectKey must be kebab-case"
|
|
1198
1372
|
}),
|
|
1199
1373
|
_ts_metadata4("design:type", String)
|
|
1200
1374
|
], ListCatalogEntriesQueryDto.prototype, "projectKey", void 0);
|
|
@@ -1219,7 +1393,7 @@ var ReviewCatalogEntryDto = class {
|
|
|
1219
1393
|
_ts_decorate4([
|
|
1220
1394
|
IsString(),
|
|
1221
1395
|
IsIn(REVIEW_STATUSES, {
|
|
1222
|
-
message: "discoveryStatus
|
|
1396
|
+
message: "discoveryStatus must be pending|approved|outdated|obsolete"
|
|
1223
1397
|
}),
|
|
1224
1398
|
_ts_metadata4("design:type", typeof DiscoveryStatus === "undefined" ? Object : DiscoveryStatus)
|
|
1225
1399
|
], ReviewCatalogEntryDto.prototype, "discoveryStatus", void 0);
|
|
@@ -1617,6 +1791,7 @@ __name(buildCatalogEntriesController, "buildCatalogEntriesController");
|
|
|
1617
1791
|
|
|
1618
1792
|
// src/catalog/promotions.service.ts
|
|
1619
1793
|
import { Inject as Inject6, Injectable as Injectable5, NotFoundException as NotFoundException4 } from "@nestjs/common";
|
|
1794
|
+
import { CATALOG_ERROR_CODES as CATALOG_ERROR_CODES4 } from "@saasicat/types";
|
|
1620
1795
|
function _ts_decorate7(decorators, target, key, desc) {
|
|
1621
1796
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
1622
1797
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
@@ -1650,7 +1825,13 @@ var PromotionsService = class {
|
|
|
1650
1825
|
async getById(id) {
|
|
1651
1826
|
const row = await this.repo.findById(id);
|
|
1652
1827
|
if (!row) {
|
|
1653
|
-
throw new NotFoundException4(
|
|
1828
|
+
throw new NotFoundException4({
|
|
1829
|
+
code: CATALOG_ERROR_CODES4.PROMOTION_NOT_FOUND,
|
|
1830
|
+
message: `Promotion '${id}' not found`,
|
|
1831
|
+
params: {
|
|
1832
|
+
promotionId: id
|
|
1833
|
+
}
|
|
1834
|
+
});
|
|
1654
1835
|
}
|
|
1655
1836
|
return row;
|
|
1656
1837
|
}
|
|
@@ -1660,14 +1841,26 @@ var PromotionsService = class {
|
|
|
1660
1841
|
async update(id, data) {
|
|
1661
1842
|
const existing = await this.repo.findById(id);
|
|
1662
1843
|
if (!existing) {
|
|
1663
|
-
throw new NotFoundException4(
|
|
1844
|
+
throw new NotFoundException4({
|
|
1845
|
+
code: CATALOG_ERROR_CODES4.PROMOTION_NOT_FOUND,
|
|
1846
|
+
message: `Promotion '${id}' not found`,
|
|
1847
|
+
params: {
|
|
1848
|
+
promotionId: id
|
|
1849
|
+
}
|
|
1850
|
+
});
|
|
1664
1851
|
}
|
|
1665
1852
|
return this.repo.update(id, data);
|
|
1666
1853
|
}
|
|
1667
1854
|
async delete(id) {
|
|
1668
1855
|
const existing = await this.repo.findById(id);
|
|
1669
1856
|
if (!existing) {
|
|
1670
|
-
throw new NotFoundException4(
|
|
1857
|
+
throw new NotFoundException4({
|
|
1858
|
+
code: CATALOG_ERROR_CODES4.PROMOTION_NOT_FOUND,
|
|
1859
|
+
message: `Promotion '${id}' not found`,
|
|
1860
|
+
params: {
|
|
1861
|
+
promotionId: id
|
|
1862
|
+
}
|
|
1863
|
+
});
|
|
1671
1864
|
}
|
|
1672
1865
|
await this.repo.delete(id);
|
|
1673
1866
|
}
|
|
@@ -1721,7 +1914,7 @@ var ListPromotionsQueryDto = class {
|
|
|
1721
1914
|
_ts_decorate8([
|
|
1722
1915
|
IsString2(),
|
|
1723
1916
|
Matches2(PROJECT_KEY_PATTERN2, {
|
|
1724
|
-
message: "projectKey
|
|
1917
|
+
message: "projectKey must be kebab-case"
|
|
1725
1918
|
}),
|
|
1726
1919
|
_ts_metadata8("design:type", String)
|
|
1727
1920
|
], ListPromotionsQueryDto.prototype, "projectKey", void 0);
|
|
@@ -1789,14 +1982,14 @@ _ts_decorate8([
|
|
|
1789
1982
|
_ts_decorate8([
|
|
1790
1983
|
IsString2(),
|
|
1791
1984
|
Matches2(ISO_DATE_PATTERN, {
|
|
1792
|
-
message: "validFrom
|
|
1985
|
+
message: "validFrom must be an ISO date"
|
|
1793
1986
|
}),
|
|
1794
1987
|
_ts_metadata8("design:type", String)
|
|
1795
1988
|
], CreatePromotionDto.prototype, "validFrom", void 0);
|
|
1796
1989
|
_ts_decorate8([
|
|
1797
1990
|
IsString2(),
|
|
1798
1991
|
Matches2(ISO_DATE_PATTERN, {
|
|
1799
|
-
message: "validTo
|
|
1992
|
+
message: "validTo must be an ISO date"
|
|
1800
1993
|
}),
|
|
1801
1994
|
_ts_metadata8("design:type", String)
|
|
1802
1995
|
], CreatePromotionDto.prototype, "validTo", void 0);
|
|
@@ -2115,7 +2308,7 @@ var ListMarketingSettingsQueryDto = class {
|
|
|
2115
2308
|
_ts_decorate11([
|
|
2116
2309
|
IsString3(),
|
|
2117
2310
|
Matches3(PROJECT_KEY_PATTERN3, {
|
|
2118
|
-
message: "projectKey
|
|
2311
|
+
message: "projectKey must be kebab-case"
|
|
2119
2312
|
}),
|
|
2120
2313
|
_ts_metadata11("design:type", String)
|
|
2121
2314
|
], ListMarketingSettingsQueryDto.prototype, "projectKey", void 0);
|
|
@@ -2129,7 +2322,7 @@ var UpdateMarketingSettingsDto = class {
|
|
|
2129
2322
|
_ts_decorate11([
|
|
2130
2323
|
IsString3(),
|
|
2131
2324
|
Matches3(PROJECT_KEY_PATTERN3, {
|
|
2132
|
-
message: "projectKey
|
|
2325
|
+
message: "projectKey must be kebab-case"
|
|
2133
2326
|
}),
|
|
2134
2327
|
_ts_metadata11("design:type", String)
|
|
2135
2328
|
], UpdateMarketingSettingsDto.prototype, "projectKey", void 0);
|
|
@@ -2141,7 +2334,7 @@ _ts_decorate11([
|
|
|
2141
2334
|
}),
|
|
2142
2335
|
Matches3(LOCALE_PATTERN, {
|
|
2143
2336
|
each: true,
|
|
2144
|
-
message: "Locale
|
|
2337
|
+
message: "Locale must be ISO-639-1"
|
|
2145
2338
|
}),
|
|
2146
2339
|
_ts_metadata11("design:type", Array)
|
|
2147
2340
|
], UpdateMarketingSettingsDto.prototype, "activeLocales", void 0);
|
|
@@ -2630,6 +2823,7 @@ __name(parseAsOf, "parseAsOf");
|
|
|
2630
2823
|
|
|
2631
2824
|
// src/catalog/plans.service.ts
|
|
2632
2825
|
import { Inject as Inject12, Injectable as Injectable8, NotFoundException as NotFoundException5, Optional as Optional6, UnprocessableEntityException as UnprocessableEntityException3 } from "@nestjs/common";
|
|
2826
|
+
import { CATALOG_ERROR_CODES as CATALOG_ERROR_CODES5 } from "@saasicat/types";
|
|
2633
2827
|
function _ts_decorate15(decorators, target, key, desc) {
|
|
2634
2828
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
2635
2829
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
@@ -2676,24 +2870,55 @@ var PlansService = class {
|
|
|
2676
2870
|
}
|
|
2677
2871
|
async getPlan(planId) {
|
|
2678
2872
|
const plan = await this.repo.findById(planId);
|
|
2679
|
-
if (!plan)
|
|
2873
|
+
if (!plan) {
|
|
2874
|
+
throw new NotFoundException5({
|
|
2875
|
+
code: CATALOG_ERROR_CODES5.PLAN_NOT_FOUND,
|
|
2876
|
+
message: `Plan '${planId}' not found`,
|
|
2877
|
+
params: {
|
|
2878
|
+
planId
|
|
2879
|
+
}
|
|
2880
|
+
});
|
|
2881
|
+
}
|
|
2680
2882
|
return plan;
|
|
2681
2883
|
}
|
|
2682
2884
|
async createPlan(data) {
|
|
2683
2885
|
const existing = await this.repo.findByKey(data.projectKey, data.planKey);
|
|
2684
2886
|
if (existing) {
|
|
2685
|
-
throw new UnprocessableEntityException3(
|
|
2887
|
+
throw new UnprocessableEntityException3({
|
|
2888
|
+
code: CATALOG_ERROR_CODES5.PLAN_ALREADY_EXISTS,
|
|
2889
|
+
message: `Plan '${data.planKey}' already exists in project '${data.projectKey}'`,
|
|
2890
|
+
params: {
|
|
2891
|
+
planKey: data.planKey,
|
|
2892
|
+
projectKey: data.projectKey
|
|
2893
|
+
}
|
|
2894
|
+
});
|
|
2686
2895
|
}
|
|
2687
2896
|
return this.repo.create(data);
|
|
2688
2897
|
}
|
|
2689
2898
|
async updatePlan(planId, data) {
|
|
2690
2899
|
const existing = await this.repo.findById(planId);
|
|
2691
|
-
if (!existing)
|
|
2900
|
+
if (!existing) {
|
|
2901
|
+
throw new NotFoundException5({
|
|
2902
|
+
code: CATALOG_ERROR_CODES5.PLAN_NOT_FOUND,
|
|
2903
|
+
message: `Plan '${planId}' not found`,
|
|
2904
|
+
params: {
|
|
2905
|
+
planId
|
|
2906
|
+
}
|
|
2907
|
+
});
|
|
2908
|
+
}
|
|
2692
2909
|
return this.repo.update(planId, data);
|
|
2693
2910
|
}
|
|
2694
2911
|
async softDeletePlan(planId) {
|
|
2695
2912
|
const existing = await this.repo.findById(planId);
|
|
2696
|
-
if (!existing)
|
|
2913
|
+
if (!existing) {
|
|
2914
|
+
throw new NotFoundException5({
|
|
2915
|
+
code: CATALOG_ERROR_CODES5.PLAN_NOT_FOUND,
|
|
2916
|
+
message: `Plan '${planId}' not found`,
|
|
2917
|
+
params: {
|
|
2918
|
+
planId
|
|
2919
|
+
}
|
|
2920
|
+
});
|
|
2921
|
+
}
|
|
2697
2922
|
if (existing.deletedAt !== null) return;
|
|
2698
2923
|
await this.assertNoPublishedVersions(existing.planKey, "soft-delete");
|
|
2699
2924
|
await this.repo.softDelete(planId);
|
|
@@ -2710,13 +2935,21 @@ var PlansService = class {
|
|
|
2710
2935
|
*/
|
|
2711
2936
|
async hardDeletePlan(planId) {
|
|
2712
2937
|
const existing = await this.repo.findById(planId);
|
|
2713
|
-
if (!existing)
|
|
2938
|
+
if (!existing) {
|
|
2939
|
+
throw new NotFoundException5({
|
|
2940
|
+
code: CATALOG_ERROR_CODES5.PLAN_NOT_FOUND,
|
|
2941
|
+
message: `Plan '${planId}' not found`,
|
|
2942
|
+
params: {
|
|
2943
|
+
planId
|
|
2944
|
+
}
|
|
2945
|
+
});
|
|
2946
|
+
}
|
|
2714
2947
|
await this.assertNoPublishedVersions(existing.planKey, "hard-delete");
|
|
2715
2948
|
await this.assertNoDrafts(existing.planKey);
|
|
2716
2949
|
if (typeof this.repo.hardDelete !== "function") {
|
|
2717
2950
|
throw new UnprocessableEntityException3({
|
|
2718
|
-
code:
|
|
2719
|
-
message: "
|
|
2951
|
+
code: CATALOG_ERROR_CODES5.PLAN_HARD_DELETE_NOT_IMPLEMENTED,
|
|
2952
|
+
message: "Hard delete is not implemented in the current repository. Implement PlanRepository.hardDelete."
|
|
2720
2953
|
});
|
|
2721
2954
|
}
|
|
2722
2955
|
await this.repo.hardDelete(planId);
|
|
@@ -2736,8 +2969,12 @@ var PlansService = class {
|
|
|
2736
2969
|
const live = published.filter((v) => v.supersededAt === null).length;
|
|
2737
2970
|
const superseded = published.length - live;
|
|
2738
2971
|
throw new UnprocessableEntityException3({
|
|
2739
|
-
code:
|
|
2740
|
-
message: `Plan '${planKey}'
|
|
2972
|
+
code: CATALOG_ERROR_CODES5.PLAN_HAS_PUBLISHED_VERSIONS,
|
|
2973
|
+
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.`,
|
|
2974
|
+
params: {
|
|
2975
|
+
planKey,
|
|
2976
|
+
operation: op
|
|
2977
|
+
},
|
|
2741
2978
|
publishedCount: published.length,
|
|
2742
2979
|
liveCount: live,
|
|
2743
2980
|
supersededCount: superseded
|
|
@@ -2749,8 +2986,11 @@ var PlansService = class {
|
|
|
2749
2986
|
const drafts = versions.filter((v) => v.publishedAt === null);
|
|
2750
2987
|
if (drafts.length === 0) return;
|
|
2751
2988
|
throw new UnprocessableEntityException3({
|
|
2752
|
-
code:
|
|
2753
|
-
message: `Plan '${planKey}'
|
|
2989
|
+
code: CATALOG_ERROR_CODES5.PLAN_HAS_DRAFTS,
|
|
2990
|
+
message: `Plan '${planKey}' still has ${drafts.length} open draft version(s). Discard them first (DELETE /admin/catalog/plan-versions/:id) or publish them.`,
|
|
2991
|
+
params: {
|
|
2992
|
+
planKey
|
|
2993
|
+
},
|
|
2754
2994
|
draftCount: drafts.length
|
|
2755
2995
|
});
|
|
2756
2996
|
}
|
|
@@ -2769,7 +3009,7 @@ PlansService = _ts_decorate15([
|
|
|
2769
3009
|
|
|
2770
3010
|
// src/catalog/plan-versions.service.ts
|
|
2771
3011
|
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";
|
|
3012
|
+
import { CATALOG_ERROR_CODES as CATALOG_ERROR_CODES6, isVersionEditable as isVersionEditable2 } from "@saasicat/types";
|
|
2773
3013
|
function _ts_decorate16(decorators, target, key, desc) {
|
|
2774
3014
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
2775
3015
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
@@ -2808,7 +3048,7 @@ var PlanVersionsService = class _PlanVersionsService {
|
|
|
2808
3048
|
this.mode = config.strictModeCheckMode ?? "blocking";
|
|
2809
3049
|
this.marketedOnly = new Set(config.marketedOnlyFeatures ?? []);
|
|
2810
3050
|
if (this.mode === "blocking" && !hasDiscoverySnapshotSource(this.snapshot, this.scanner)) {
|
|
2811
|
-
this.logger.error("PlanVersionsService: strictModeCheckMode=blocking
|
|
3051
|
+
this.logger.error("PlanVersionsService: strictModeCheckMode=blocking without a DiscoverySnapshot source \u2014 degrading to warn-only. Wire DiscoveryModule (#25).");
|
|
2812
3052
|
this.mode = "warn-only";
|
|
2813
3053
|
}
|
|
2814
3054
|
const required = [
|
|
@@ -2822,7 +3062,7 @@ var PlanVersionsService = class _PlanVersionsService {
|
|
|
2822
3062
|
];
|
|
2823
3063
|
for (const key of required) {
|
|
2824
3064
|
if (typeof this.repo[key] !== "function") {
|
|
2825
|
-
throw new Error(`PlanVersionsService: PlanRepository.${String(key)}
|
|
3065
|
+
throw new Error(`PlanVersionsService: PlanRepository.${String(key)} is missing \u2014 the repository does not implement the lifecycle methods.`);
|
|
2826
3066
|
}
|
|
2827
3067
|
}
|
|
2828
3068
|
}
|
|
@@ -2838,18 +3078,27 @@ var PlanVersionsService = class _PlanVersionsService {
|
|
|
2838
3078
|
async discardPlanDraft(versionId) {
|
|
2839
3079
|
const existing = await this.repo.findVersionById(versionId);
|
|
2840
3080
|
if (!existing) {
|
|
2841
|
-
throw new NotFoundException6(
|
|
3081
|
+
throw new NotFoundException6({
|
|
3082
|
+
code: CATALOG_ERROR_CODES6.PLAN_VERSION_NOT_FOUND,
|
|
3083
|
+
message: `PlanVersion '${versionId}' not found`,
|
|
3084
|
+
params: {
|
|
3085
|
+
versionId
|
|
3086
|
+
}
|
|
3087
|
+
});
|
|
2842
3088
|
}
|
|
2843
3089
|
if (existing.publishedAt !== null) {
|
|
2844
3090
|
throw new UnprocessableEntityException4({
|
|
2845
|
-
code:
|
|
2846
|
-
message: `PlanVersion '${versionId}'
|
|
3091
|
+
code: CATALOG_ERROR_CODES6.PLAN_VERSION_ALREADY_PUBLISHED,
|
|
3092
|
+
message: `PlanVersion '${versionId}' is already published and cannot be discarded.`,
|
|
3093
|
+
params: {
|
|
3094
|
+
versionId
|
|
3095
|
+
}
|
|
2847
3096
|
});
|
|
2848
3097
|
}
|
|
2849
3098
|
if (typeof this.repo.deletePlanVersionDraft !== "function") {
|
|
2850
3099
|
throw new UnprocessableEntityException4({
|
|
2851
|
-
code:
|
|
2852
|
-
message: "Discard
|
|
3100
|
+
code: CATALOG_ERROR_CODES6.PLAN_VERSION_DISCARD_NOT_IMPLEMENTED,
|
|
3101
|
+
message: "Discard is not implemented in the current repository. Implement PlanRepository.deletePlanVersionDraft."
|
|
2853
3102
|
});
|
|
2854
3103
|
}
|
|
2855
3104
|
await this.repo.deletePlanVersionDraft(versionId);
|
|
@@ -2862,7 +3111,13 @@ var PlanVersionsService = class _PlanVersionsService {
|
|
|
2862
3111
|
async listPlanVersions(planUuid) {
|
|
2863
3112
|
const plan = await this.repo.findById(planUuid);
|
|
2864
3113
|
if (!plan) {
|
|
2865
|
-
throw new NotFoundException6(
|
|
3114
|
+
throw new NotFoundException6({
|
|
3115
|
+
code: CATALOG_ERROR_CODES6.PLAN_NOT_FOUND,
|
|
3116
|
+
message: `Plan '${planUuid}' not found`,
|
|
3117
|
+
params: {
|
|
3118
|
+
planId: planUuid
|
|
3119
|
+
}
|
|
3120
|
+
});
|
|
2866
3121
|
}
|
|
2867
3122
|
const versions = await this.repo.listVersions(plan.planKey);
|
|
2868
3123
|
return this.annotateEditability(versions);
|
|
@@ -2870,7 +3125,13 @@ var PlanVersionsService = class _PlanVersionsService {
|
|
|
2870
3125
|
async getPlanVersion(versionId) {
|
|
2871
3126
|
const version = await this.repo.findVersionById(versionId);
|
|
2872
3127
|
if (!version) {
|
|
2873
|
-
throw new NotFoundException6(
|
|
3128
|
+
throw new NotFoundException6({
|
|
3129
|
+
code: CATALOG_ERROR_CODES6.PLAN_VERSION_NOT_FOUND,
|
|
3130
|
+
message: `PlanVersion '${versionId}' not found`,
|
|
3131
|
+
params: {
|
|
3132
|
+
versionId
|
|
3133
|
+
}
|
|
3134
|
+
});
|
|
2874
3135
|
}
|
|
2875
3136
|
const [annotated] = await this.annotateEditability([
|
|
2876
3137
|
version
|
|
@@ -2908,12 +3169,25 @@ var PlanVersionsService = class _PlanVersionsService {
|
|
|
2908
3169
|
async createPlanDraft(data) {
|
|
2909
3170
|
const plan = await this.repo.findById(data.planId);
|
|
2910
3171
|
if (!plan) {
|
|
2911
|
-
throw new NotFoundException6(
|
|
3172
|
+
throw new NotFoundException6({
|
|
3173
|
+
code: CATALOG_ERROR_CODES6.PLAN_NOT_FOUND,
|
|
3174
|
+
message: `Plan '${data.planId}' not found`,
|
|
3175
|
+
params: {
|
|
3176
|
+
planId: data.planId
|
|
3177
|
+
}
|
|
3178
|
+
});
|
|
2912
3179
|
}
|
|
2913
3180
|
const planKey = plan.planKey;
|
|
2914
3181
|
const existingDraft = await this.repo.findCurrentDraft(planKey);
|
|
2915
3182
|
if (existingDraft) {
|
|
2916
|
-
throw new UnprocessableEntityException4(
|
|
3183
|
+
throw new UnprocessableEntityException4({
|
|
3184
|
+
code: CATALOG_ERROR_CODES6.PLAN_DRAFT_ALREADY_EXISTS,
|
|
3185
|
+
message: `Plan '${planKey}' already has a draft version v${existingDraft.version}; publish or discard it first`,
|
|
3186
|
+
params: {
|
|
3187
|
+
planKey,
|
|
3188
|
+
draftVersion: existingDraft.version
|
|
3189
|
+
}
|
|
3190
|
+
});
|
|
2917
3191
|
}
|
|
2918
3192
|
let baseVersionId = data.baseVersionId;
|
|
2919
3193
|
if (baseVersionId === void 0) {
|
|
@@ -2938,7 +3212,13 @@ var PlanVersionsService = class _PlanVersionsService {
|
|
|
2938
3212
|
async updatePlanDraft(versionId, data) {
|
|
2939
3213
|
const existing = await this.repo.findVersionById(versionId);
|
|
2940
3214
|
if (!existing) {
|
|
2941
|
-
throw new NotFoundException6(
|
|
3215
|
+
throw new NotFoundException6({
|
|
3216
|
+
code: CATALOG_ERROR_CODES6.PLAN_VERSION_NOT_FOUND,
|
|
3217
|
+
message: `PlanVersion '${versionId}' not found`,
|
|
3218
|
+
params: {
|
|
3219
|
+
versionId
|
|
3220
|
+
}
|
|
3221
|
+
});
|
|
2942
3222
|
}
|
|
2943
3223
|
const [annotated] = await this.annotateEditability([
|
|
2944
3224
|
existing
|
|
@@ -2946,8 +3226,11 @@ var PlanVersionsService = class _PlanVersionsService {
|
|
|
2946
3226
|
const { editable } = isVersionEditable2(annotated);
|
|
2947
3227
|
if (!editable) {
|
|
2948
3228
|
throw new UnprocessableEntityException4({
|
|
2949
|
-
code:
|
|
2950
|
-
message: `PlanVersion '${versionId}'
|
|
3229
|
+
code: CATALOG_ERROR_CODES6.PLAN_VERSION_NOT_EDITABLE,
|
|
3230
|
+
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.`,
|
|
3231
|
+
params: {
|
|
3232
|
+
versionId
|
|
3233
|
+
}
|
|
2951
3234
|
});
|
|
2952
3235
|
}
|
|
2953
3236
|
const merged = {
|
|
@@ -2965,10 +3248,22 @@ var PlanVersionsService = class _PlanVersionsService {
|
|
|
2965
3248
|
async publishPlanVersion(versionId, publishMeta) {
|
|
2966
3249
|
const draft = await this.repo.findVersionById(versionId);
|
|
2967
3250
|
if (!draft) {
|
|
2968
|
-
throw new NotFoundException6(
|
|
3251
|
+
throw new NotFoundException6({
|
|
3252
|
+
code: CATALOG_ERROR_CODES6.PLAN_VERSION_NOT_FOUND,
|
|
3253
|
+
message: `PlanVersion '${versionId}' not found`,
|
|
3254
|
+
params: {
|
|
3255
|
+
versionId
|
|
3256
|
+
}
|
|
3257
|
+
});
|
|
2969
3258
|
}
|
|
2970
3259
|
if (draft.publishedAt !== null) {
|
|
2971
|
-
throw new UnprocessableEntityException4(
|
|
3260
|
+
throw new UnprocessableEntityException4({
|
|
3261
|
+
code: CATALOG_ERROR_CODES6.PLAN_VERSION_ALREADY_PUBLISHED,
|
|
3262
|
+
message: `PlanVersion '${versionId}' is already published`,
|
|
3263
|
+
params: {
|
|
3264
|
+
versionId
|
|
3265
|
+
}
|
|
3266
|
+
});
|
|
2972
3267
|
}
|
|
2973
3268
|
const warnings = await this.runStrictCheck({
|
|
2974
3269
|
features: draft.features,
|
|
@@ -2980,8 +3275,8 @@ var PlanVersionsService = class _PlanVersionsService {
|
|
|
2980
3275
|
const yearly = Number.parseFloat(String(draft.yearlyNet ?? "0"));
|
|
2981
3276
|
if (monthly <= 0 || yearly <= 0) {
|
|
2982
3277
|
throw new UnprocessableEntityException4({
|
|
2983
|
-
code:
|
|
2984
|
-
message: "
|
|
3278
|
+
code: CATALOG_ERROR_CODES6.PLAN_VERSION_ZERO_PRICE,
|
|
3279
|
+
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
3280
|
monthlyNet: String(draft.monthlyNet),
|
|
2986
3281
|
yearlyNet: String(draft.yearlyNet)
|
|
2987
3282
|
});
|
|
@@ -2991,23 +3286,30 @@ var PlanVersionsService = class _PlanVersionsService {
|
|
|
2991
3286
|
const validFromInput = publishMeta.validFrom ?? draft.validFrom;
|
|
2992
3287
|
if (!validFromInput) {
|
|
2993
3288
|
throw new UnprocessableEntityException4({
|
|
2994
|
-
code:
|
|
2995
|
-
message: "
|
|
3289
|
+
code: CATALOG_ERROR_CODES6.PLAN_VERSION_VALID_FROM_REQUIRED,
|
|
3290
|
+
message: "validFrom must be set when publishing (on the draft or the publish call). SPEC_V2 \xA74.2."
|
|
2996
3291
|
});
|
|
2997
3292
|
}
|
|
2998
3293
|
const validFrom = new Date(validFromInput);
|
|
2999
3294
|
if (Number.isNaN(validFrom.getTime())) {
|
|
3000
3295
|
throw new UnprocessableEntityException4({
|
|
3001
|
-
code:
|
|
3002
|
-
message: `validFrom '${validFromInput}'
|
|
3296
|
+
code: CATALOG_ERROR_CODES6.PLAN_VERSION_VALID_FROM_INVALID,
|
|
3297
|
+
message: `validFrom '${validFromInput}' is not a valid date`,
|
|
3298
|
+
params: {
|
|
3299
|
+
validFrom: validFromInput
|
|
3300
|
+
}
|
|
3003
3301
|
});
|
|
3004
3302
|
}
|
|
3005
3303
|
if (previous?.validFrom) {
|
|
3006
3304
|
const prevFrom = new Date(previous.validFrom);
|
|
3007
3305
|
if (validFrom <= prevFrom) {
|
|
3008
3306
|
throw new UnprocessableEntityException4({
|
|
3009
|
-
code:
|
|
3010
|
-
message: `validFrom (${validFrom.toISOString()})
|
|
3307
|
+
code: CATALOG_ERROR_CODES6.PLAN_VERSION_VALID_FROM_NOT_AFTER_PREVIOUS,
|
|
3308
|
+
message: `validFrom (${validFrom.toISOString()}) must be strictly after the validFrom of the previous version (${previous.validFrom}).`,
|
|
3309
|
+
params: {
|
|
3310
|
+
validFrom: validFrom.toISOString(),
|
|
3311
|
+
previousValidFrom: previous.validFrom
|
|
3312
|
+
}
|
|
3011
3313
|
});
|
|
3012
3314
|
}
|
|
3013
3315
|
if (previous.validUntil) {
|
|
@@ -3016,8 +3318,11 @@ var PlanVersionsService = class _PlanVersionsService {
|
|
|
3016
3318
|
const requiredStart = new Date(prevUntil.getTime() + dayMs);
|
|
3017
3319
|
if (validFrom.getTime() !== requiredStart.getTime()) {
|
|
3018
3320
|
throw new UnprocessableEntityException4({
|
|
3019
|
-
code:
|
|
3020
|
-
message: `
|
|
3321
|
+
code: CATALOG_ERROR_CODES6.PLAN_VERSION_VALID_FROM_NOT_GAPLESS,
|
|
3322
|
+
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)}.`,
|
|
3323
|
+
params: {
|
|
3324
|
+
received: validFrom.toISOString().slice(0, 10)
|
|
3325
|
+
},
|
|
3021
3326
|
requiredValidFrom: requiredStart.toISOString().slice(0, 10),
|
|
3022
3327
|
previousValidUntil: previous.validUntil
|
|
3023
3328
|
});
|
|
@@ -3028,14 +3333,21 @@ var PlanVersionsService = class _PlanVersionsService {
|
|
|
3028
3333
|
const validUntil = validUntilInput ? new Date(validUntilInput) : null;
|
|
3029
3334
|
if (validUntil && Number.isNaN(validUntil.getTime())) {
|
|
3030
3335
|
throw new UnprocessableEntityException4({
|
|
3031
|
-
code:
|
|
3032
|
-
message: `validUntil '${validUntilInput}'
|
|
3336
|
+
code: CATALOG_ERROR_CODES6.PLAN_VERSION_VALID_UNTIL_INVALID,
|
|
3337
|
+
message: `validUntil '${validUntilInput}' is not a valid date`,
|
|
3338
|
+
params: {
|
|
3339
|
+
validUntil: validUntilInput
|
|
3340
|
+
}
|
|
3033
3341
|
});
|
|
3034
3342
|
}
|
|
3035
3343
|
if (validUntil && validUntil <= validFrom) {
|
|
3036
3344
|
throw new UnprocessableEntityException4({
|
|
3037
|
-
code:
|
|
3038
|
-
message: `validUntil (${validUntil.toISOString()})
|
|
3345
|
+
code: CATALOG_ERROR_CODES6.PLAN_VERSION_VALID_UNTIL_BEFORE_FROM,
|
|
3346
|
+
message: `validUntil (${validUntil.toISOString()}) must be strictly after validFrom (${validFrom.toISOString()}).`,
|
|
3347
|
+
params: {
|
|
3348
|
+
validUntil: validUntil.toISOString(),
|
|
3349
|
+
validFrom: validFrom.toISOString()
|
|
3350
|
+
}
|
|
3039
3351
|
});
|
|
3040
3352
|
}
|
|
3041
3353
|
const fieldsFor = /* @__PURE__ */ __name((v) => ({
|
|
@@ -3052,8 +3364,8 @@ var PlanVersionsService = class _PlanVersionsService {
|
|
|
3052
3364
|
};
|
|
3053
3365
|
if (!diff.nonRegressive && !publishMeta.forceRegressive) {
|
|
3054
3366
|
throw new UnprocessableEntityException4({
|
|
3055
|
-
code:
|
|
3056
|
-
message: "
|
|
3367
|
+
code: CATALOG_ERROR_CODES6.PLAN_VERSION_REGRESSION,
|
|
3368
|
+
message: "This plan version is regressive (feature removed / quota lowered / price raised). Publishing requires an explicit `forceRegressive: true` (UI confirmation dialog with MFA).",
|
|
3057
3369
|
changes: diff.changes
|
|
3058
3370
|
});
|
|
3059
3371
|
}
|
|
@@ -3088,36 +3400,51 @@ var PlanVersionsService = class _PlanVersionsService {
|
|
|
3088
3400
|
async terminatePlanVersion(versionId, endsAt) {
|
|
3089
3401
|
const existing = await this.repo.findVersionById(versionId);
|
|
3090
3402
|
if (!existing) {
|
|
3091
|
-
throw new NotFoundException6(
|
|
3403
|
+
throw new NotFoundException6({
|
|
3404
|
+
code: CATALOG_ERROR_CODES6.PLAN_VERSION_NOT_FOUND,
|
|
3405
|
+
message: `PlanVersion '${versionId}' not found`,
|
|
3406
|
+
params: {
|
|
3407
|
+
versionId
|
|
3408
|
+
}
|
|
3409
|
+
});
|
|
3092
3410
|
}
|
|
3093
3411
|
if (existing.publishedAt === null) {
|
|
3094
3412
|
throw new UnprocessableEntityException4({
|
|
3095
|
-
code:
|
|
3096
|
-
message: `PlanVersion '${versionId}'
|
|
3413
|
+
code: CATALOG_ERROR_CODES6.PLAN_VERSION_NOT_PUBLISHED,
|
|
3414
|
+
message: `PlanVersion '${versionId}' is not published and cannot be terminated.`,
|
|
3415
|
+
params: {
|
|
3416
|
+
versionId
|
|
3417
|
+
}
|
|
3097
3418
|
});
|
|
3098
3419
|
}
|
|
3099
3420
|
if (existing.supersededAt !== null) {
|
|
3100
3421
|
throw new UnprocessableEntityException4({
|
|
3101
|
-
code:
|
|
3102
|
-
message: `PlanVersion '${versionId}'
|
|
3422
|
+
code: CATALOG_ERROR_CODES6.PLAN_VERSION_SUPERSEDED,
|
|
3423
|
+
message: `PlanVersion '${versionId}' has already been superseded by a newer version (supersededAt) and cannot be terminated.`,
|
|
3424
|
+
params: {
|
|
3425
|
+
versionId
|
|
3426
|
+
}
|
|
3103
3427
|
});
|
|
3104
3428
|
}
|
|
3105
3429
|
if (Number.isNaN(endsAt.getTime())) {
|
|
3106
3430
|
throw new UnprocessableEntityException4({
|
|
3107
|
-
code:
|
|
3108
|
-
message: "endsAt
|
|
3431
|
+
code: CATALOG_ERROR_CODES6.PLAN_TERMINATE_INVALID_DATE,
|
|
3432
|
+
message: "endsAt is not a valid date."
|
|
3109
3433
|
});
|
|
3110
3434
|
}
|
|
3111
3435
|
if (endsAt.getTime() <= Date.now()) {
|
|
3112
3436
|
throw new UnprocessableEntityException4({
|
|
3113
|
-
code:
|
|
3114
|
-
message: `endsAt (${endsAt.toISOString()})
|
|
3437
|
+
code: CATALOG_ERROR_CODES6.PLAN_TERMINATE_DATE_NOT_FUTURE,
|
|
3438
|
+
message: `endsAt (${endsAt.toISOString()}) must lie strictly in the future.`,
|
|
3439
|
+
params: {
|
|
3440
|
+
endsAt: endsAt.toISOString()
|
|
3441
|
+
}
|
|
3115
3442
|
});
|
|
3116
3443
|
}
|
|
3117
3444
|
if (typeof this.repo.terminate !== "function") {
|
|
3118
3445
|
throw new UnprocessableEntityException4({
|
|
3119
|
-
code:
|
|
3120
|
-
message: "Terminate
|
|
3446
|
+
code: CATALOG_ERROR_CODES6.PLAN_TERMINATE_NOT_IMPLEMENTED,
|
|
3447
|
+
message: "Terminate is not implemented in the current repository. Implement PlanRepository.terminate."
|
|
3121
3448
|
});
|
|
3122
3449
|
}
|
|
3123
3450
|
return this.repo.terminate(versionId, endsAt);
|
|
@@ -3135,8 +3462,8 @@ var PlanVersionsService = class _PlanVersionsService {
|
|
|
3135
3462
|
if (blockingStrictModeWarnings(warnings).length === 0) return;
|
|
3136
3463
|
if (this.mode === "blocking") {
|
|
3137
3464
|
throw new UnprocessableEntityException4({
|
|
3138
|
-
code:
|
|
3139
|
-
message: "
|
|
3465
|
+
code: CATALOG_ERROR_CODES6.STRICT_MODE_VIOLATIONS,
|
|
3466
|
+
message: "The strict-mode check found drift against the discovery snapshot.",
|
|
3140
3467
|
warnings
|
|
3141
3468
|
});
|
|
3142
3469
|
}
|
|
@@ -3200,7 +3527,7 @@ var CreateBundleDto = class {
|
|
|
3200
3527
|
_ts_decorate17([
|
|
3201
3528
|
IsString4(),
|
|
3202
3529
|
Matches4(PROJECT_KEY_PATTERN4, {
|
|
3203
|
-
message: 'projectKey
|
|
3530
|
+
message: 'projectKey must be kebab-case (e.g. "my-app")'
|
|
3204
3531
|
}),
|
|
3205
3532
|
MaxLength3(64),
|
|
3206
3533
|
_ts_metadata17("design:type", String)
|
|
@@ -3208,7 +3535,7 @@ _ts_decorate17([
|
|
|
3208
3535
|
_ts_decorate17([
|
|
3209
3536
|
IsString4(),
|
|
3210
3537
|
Matches4(KEY_PATTERN, {
|
|
3211
|
-
message: 'bundleKey
|
|
3538
|
+
message: 'bundleKey must be SCREAMING_SNAKE_CASE (e.g. "BANKING")'
|
|
3212
3539
|
}),
|
|
3213
3540
|
MaxLength3(64),
|
|
3214
3541
|
_ts_metadata17("design:type", String)
|
|
@@ -3308,7 +3635,7 @@ _ts_decorate17([
|
|
|
3308
3635
|
}),
|
|
3309
3636
|
Matches4(FEATURE_KEY_PATTERN, {
|
|
3310
3637
|
each: true,
|
|
3311
|
-
message: "features
|
|
3638
|
+
message: "features entries must be SCREAMING_SNAKE_CASE"
|
|
3312
3639
|
}),
|
|
3313
3640
|
_ts_metadata17("design:type", Array)
|
|
3314
3641
|
], CreateBundleVersionDraftDto.prototype, "features", void 0);
|
|
@@ -3334,7 +3661,7 @@ _ts_decorate17([
|
|
|
3334
3661
|
no_symbols: false
|
|
3335
3662
|
}),
|
|
3336
3663
|
Matches4(DECIMAL_PATTERN, {
|
|
3337
|
-
message: 'monthlyNet
|
|
3664
|
+
message: 'monthlyNet must be a decimal with at most 2 fraction digits (e.g. "9.90")'
|
|
3338
3665
|
}),
|
|
3339
3666
|
_ts_metadata17("design:type", Object)
|
|
3340
3667
|
], CreateBundleVersionDraftDto.prototype, "monthlyNet", void 0);
|
|
@@ -3345,7 +3672,7 @@ _ts_decorate17([
|
|
|
3345
3672
|
no_symbols: false
|
|
3346
3673
|
}),
|
|
3347
3674
|
Matches4(DECIMAL_PATTERN, {
|
|
3348
|
-
message: "yearlyNet
|
|
3675
|
+
message: "yearlyNet must be a decimal with at most 2 fraction digits"
|
|
3349
3676
|
}),
|
|
3350
3677
|
_ts_metadata17("design:type", Object)
|
|
3351
3678
|
], CreateBundleVersionDraftDto.prototype, "yearlyNet", void 0);
|
|
@@ -3369,7 +3696,7 @@ _ts_decorate17([
|
|
|
3369
3696
|
IsOptional3(),
|
|
3370
3697
|
ValidateIf((_o, value) => value !== null),
|
|
3371
3698
|
Matches4(ISO_DATE_PATTERN2, {
|
|
3372
|
-
message: "validFrom
|
|
3699
|
+
message: "validFrom must be an ISO date (YYYY-MM-DD)"
|
|
3373
3700
|
}),
|
|
3374
3701
|
_ts_metadata17("design:type", Object)
|
|
3375
3702
|
], CreateBundleVersionDraftDto.prototype, "validFrom", void 0);
|
|
@@ -3377,7 +3704,7 @@ _ts_decorate17([
|
|
|
3377
3704
|
IsOptional3(),
|
|
3378
3705
|
ValidateIf((_o, value) => value !== null),
|
|
3379
3706
|
Matches4(ISO_DATE_PATTERN2, {
|
|
3380
|
-
message: "validUntil
|
|
3707
|
+
message: "validUntil must be an ISO date (YYYY-MM-DD)"
|
|
3381
3708
|
}),
|
|
3382
3709
|
_ts_metadata17("design:type", Object)
|
|
3383
3710
|
], CreateBundleVersionDraftDto.prototype, "validUntil", void 0);
|
|
@@ -3449,7 +3776,7 @@ _ts_decorate17([
|
|
|
3449
3776
|
IsOptional3(),
|
|
3450
3777
|
ValidateIf((_o, value) => value !== null),
|
|
3451
3778
|
Matches4(ISO_DATE_PATTERN2, {
|
|
3452
|
-
message: "validFrom
|
|
3779
|
+
message: "validFrom must be an ISO date (YYYY-MM-DD)"
|
|
3453
3780
|
}),
|
|
3454
3781
|
_ts_metadata17("design:type", Object)
|
|
3455
3782
|
], UpdateBundleVersionDraftDto.prototype, "validFrom", void 0);
|
|
@@ -3457,7 +3784,7 @@ _ts_decorate17([
|
|
|
3457
3784
|
IsOptional3(),
|
|
3458
3785
|
ValidateIf((_o, value) => value !== null),
|
|
3459
3786
|
Matches4(ISO_DATE_PATTERN2, {
|
|
3460
|
-
message: "validUntil
|
|
3787
|
+
message: "validUntil must be an ISO date (YYYY-MM-DD)"
|
|
3461
3788
|
}),
|
|
3462
3789
|
_ts_metadata17("design:type", Object)
|
|
3463
3790
|
], UpdateBundleVersionDraftDto.prototype, "validUntil", void 0);
|
|
@@ -3494,7 +3821,7 @@ _ts_decorate17([
|
|
|
3494
3821
|
IsOptional3(),
|
|
3495
3822
|
ValidateIf((_o, value) => value !== null),
|
|
3496
3823
|
Matches4(ISO_DATE_PATTERN2, {
|
|
3497
|
-
message: "validFrom
|
|
3824
|
+
message: "validFrom must be an ISO date (YYYY-MM-DD)"
|
|
3498
3825
|
}),
|
|
3499
3826
|
_ts_metadata17("design:type", Object)
|
|
3500
3827
|
], PublishBundleVersionDto.prototype, "validFrom", void 0);
|
|
@@ -3502,7 +3829,7 @@ _ts_decorate17([
|
|
|
3502
3829
|
IsOptional3(),
|
|
3503
3830
|
ValidateIf((_o, value) => value !== null),
|
|
3504
3831
|
Matches4(ISO_DATE_PATTERN2, {
|
|
3505
|
-
message: "validUntil
|
|
3832
|
+
message: "validUntil must be an ISO date (YYYY-MM-DD)"
|
|
3506
3833
|
}),
|
|
3507
3834
|
_ts_metadata17("design:type", Object)
|
|
3508
3835
|
], PublishBundleVersionDto.prototype, "validUntil", void 0);
|
|
@@ -3804,7 +4131,7 @@ var CreateMarketingProjectionDto = class {
|
|
|
3804
4131
|
_ts_decorate19([
|
|
3805
4132
|
IsString5(),
|
|
3806
4133
|
Matches5(PROJECT_KEY_PATTERN5, {
|
|
3807
|
-
message: "projectKey
|
|
4134
|
+
message: "projectKey must be kebab-case"
|
|
3808
4135
|
}),
|
|
3809
4136
|
MaxLength4(64),
|
|
3810
4137
|
_ts_metadata19("design:type", String)
|
|
@@ -3822,7 +4149,7 @@ _ts_decorate19([
|
|
|
3822
4149
|
IsOptional4(),
|
|
3823
4150
|
IsString5(),
|
|
3824
4151
|
Matches5(LOCALE_PATTERN2, {
|
|
3825
|
-
message: 'locale
|
|
4152
|
+
message: 'locale must be ISO-639-1 (e.g. "de", "en", "de-AT")'
|
|
3826
4153
|
}),
|
|
3827
4154
|
_ts_metadata19("design:type", String)
|
|
3828
4155
|
], CreateMarketingProjectionDto.prototype, "locale", void 0);
|
|
@@ -4151,7 +4478,7 @@ var CreatePlanDto = class {
|
|
|
4151
4478
|
_ts_decorate21([
|
|
4152
4479
|
IsString6(),
|
|
4153
4480
|
Matches6(PROJECT_KEY_PATTERN6, {
|
|
4154
|
-
message: 'projectKey
|
|
4481
|
+
message: 'projectKey must be kebab-case (e.g. "my-app")'
|
|
4155
4482
|
}),
|
|
4156
4483
|
MaxLength5(64),
|
|
4157
4484
|
_ts_metadata21("design:type", String)
|
|
@@ -4159,7 +4486,7 @@ _ts_decorate21([
|
|
|
4159
4486
|
_ts_decorate21([
|
|
4160
4487
|
IsString6(),
|
|
4161
4488
|
Matches6(KEY_PATTERN2, {
|
|
4162
|
-
message: 'planKey
|
|
4489
|
+
message: 'planKey must be SCREAMING_SNAKE_CASE (e.g. "STARTER")'
|
|
4163
4490
|
}),
|
|
4164
4491
|
MaxLength5(64),
|
|
4165
4492
|
_ts_metadata21("design:type", String)
|
|
@@ -4268,7 +4595,7 @@ _ts_decorate22([
|
|
|
4268
4595
|
}),
|
|
4269
4596
|
Matches7(FEATURE_KEY_PATTERN2, {
|
|
4270
4597
|
each: true,
|
|
4271
|
-
message: "features
|
|
4598
|
+
message: "features entries must be SCREAMING_SNAKE_CASE"
|
|
4272
4599
|
}),
|
|
4273
4600
|
_ts_metadata22("design:type", Array)
|
|
4274
4601
|
], CreatePlanVersionDraftDto.prototype, "features", void 0);
|
|
@@ -4280,7 +4607,7 @@ _ts_decorate22([
|
|
|
4280
4607
|
}),
|
|
4281
4608
|
Matches7(FEATURE_KEY_PATTERN2, {
|
|
4282
4609
|
each: true,
|
|
4283
|
-
message: "bundles
|
|
4610
|
+
message: "bundles entries must be SCREAMING_SNAKE_CASE"
|
|
4284
4611
|
}),
|
|
4285
4612
|
_ts_metadata22("design:type", Array)
|
|
4286
4613
|
], CreatePlanVersionDraftDto.prototype, "bundles", void 0);
|
|
@@ -4290,13 +4617,13 @@ _ts_decorate22([
|
|
|
4290
4617
|
], CreatePlanVersionDraftDto.prototype, "quotas", void 0);
|
|
4291
4618
|
_ts_decorate22([
|
|
4292
4619
|
Matches7(DECIMAL_PATTERN2, {
|
|
4293
|
-
message: 'monthlyNet
|
|
4620
|
+
message: 'monthlyNet must be a decimal with at most 2 fraction digits (e.g. "9.90")'
|
|
4294
4621
|
}),
|
|
4295
4622
|
_ts_metadata22("design:type", String)
|
|
4296
4623
|
], CreatePlanVersionDraftDto.prototype, "monthlyNet", void 0);
|
|
4297
4624
|
_ts_decorate22([
|
|
4298
4625
|
Matches7(DECIMAL_PATTERN2, {
|
|
4299
|
-
message: "yearlyNet
|
|
4626
|
+
message: "yearlyNet must be a decimal with at most 2 fraction digits"
|
|
4300
4627
|
}),
|
|
4301
4628
|
_ts_metadata22("design:type", String)
|
|
4302
4629
|
], CreatePlanVersionDraftDto.prototype, "yearlyNet", void 0);
|
|
@@ -4541,7 +4868,7 @@ function buildPlansController(guards) {
|
|
|
4541
4868
|
}
|
|
4542
4869
|
assertVersionsService() {
|
|
4543
4870
|
if (!this.versionsService) {
|
|
4544
|
-
throw new Error("PlanVersionsService
|
|
4871
|
+
throw new Error("PlanVersionsService is not registered \u2014 the app has not enabled the plan lifecycle editor.");
|
|
4545
4872
|
}
|
|
4546
4873
|
}
|
|
4547
4874
|
};
|