@saasicat/core 1.0.0-rc.0 → 1.0.0-rc.10
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/README.md +21 -0
- package/dist/.build-stamp +1 -1
- package/dist/index.cjs +491 -42
- package/dist/index.d.cts +1226 -380
- package/dist/index.d.ts +1226 -380
- package/dist/index.js +469 -41
- package/package.json +49 -48
package/dist/index.cjs
CHANGED
|
@@ -20,13 +20,18 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/index.ts
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
|
+
ACTIVE_SUBSCRIPTION_CONTRACT_STATUSES: () => ACTIVE_SUBSCRIPTION_CONTRACT_STATUSES,
|
|
23
24
|
AUTH_ERROR_CODES: () => AUTH_ERROR_CODES,
|
|
24
25
|
BILLING_ERROR_CODES: () => BILLING_ERROR_CODES,
|
|
26
|
+
BUNDLE_PRICE_LOOKUP_LIMIT: () => BUNDLE_PRICE_LOOKUP_LIMIT,
|
|
27
|
+
CATALOGUE_KEYS: () => CATALOGUE_KEYS,
|
|
25
28
|
CATALOG_ERROR_CODES: () => CATALOG_ERROR_CODES,
|
|
26
29
|
CONTRACT_ERROR_CODES: () => CONTRACT_ERROR_CODES,
|
|
27
30
|
ERROR_MESSAGES_DE: () => ERROR_MESSAGES_DE,
|
|
28
31
|
ERROR_MESSAGES_EN: () => ERROR_MESSAGES_EN,
|
|
29
32
|
FEATURE_NOT_LICENSED: () => FEATURE_NOT_LICENSED,
|
|
33
|
+
MARKETING_PRIORITY_MAX: () => MARKETING_PRIORITY_MAX,
|
|
34
|
+
MARKETING_PRIORITY_MIN: () => MARKETING_PRIORITY_MIN,
|
|
30
35
|
OTP_RATE_LIMIT_MAX_SENDS: () => OTP_RATE_LIMIT_MAX_SENDS,
|
|
31
36
|
OTP_RATE_LIMIT_WINDOW_MINUTES: () => OTP_RATE_LIMIT_WINDOW_MINUTES,
|
|
32
37
|
OTP_TTL_MINUTES: () => OTP_TTL_MINUTES,
|
|
@@ -42,27 +47,43 @@ __export(index_exports, {
|
|
|
42
47
|
REGISTRATION_ERROR_CODES: () => REGISTRATION_ERROR_CODES,
|
|
43
48
|
REGISTRATION_RESUME_TTL_MINUTES: () => REGISTRATION_RESUME_TTL_MINUTES,
|
|
44
49
|
REGISTRATION_STEP_BY_STATUS: () => REGISTRATION_STEP_BY_STATUS,
|
|
50
|
+
SETTINGS_ERROR_CODES: () => SETTINGS_ERROR_CODES,
|
|
45
51
|
SETUP_ERROR_CODES: () => SETUP_ERROR_CODES,
|
|
46
52
|
applyPromo: () => applyPromo,
|
|
47
53
|
assertPersistenceCapabilities: () => assertPersistenceCapabilities,
|
|
48
54
|
buildActivePlanVersionWhere: () => buildActivePlanVersionWhere,
|
|
49
55
|
buildActiveVersionWhere: () => buildActiveVersionWhere,
|
|
50
56
|
buildFeatureRequiresIndex: () => buildFeatureRequiresIndex,
|
|
57
|
+
bundleDraftDefaults: () => bundleDraftDefaults,
|
|
58
|
+
bundleStemDefaults: () => bundleStemDefaults,
|
|
59
|
+
canonicalJson: () => canonicalJson,
|
|
51
60
|
classifyBundleVersionDiff: () => classifyBundleVersionDiff,
|
|
52
61
|
classifyPlanDiff: () => classifyPlanDiff,
|
|
53
62
|
collectUnsatisfiedRequires: () => collectUnsatisfiedRequires,
|
|
54
63
|
coverageExcludingSelf: () => coverageExcludingSelf,
|
|
64
|
+
definedFields: () => definedFields,
|
|
65
|
+
diffSettings: () => diffSettings,
|
|
55
66
|
formatErrorMessage: () => formatErrorMessage,
|
|
56
67
|
isBundleRedundant: () => isBundleRedundant,
|
|
57
68
|
isPlatformUserExistsError: () => isPlatformUserExistsError,
|
|
58
69
|
isVersionEditable: () => isVersionEditable,
|
|
70
|
+
keepOneRecommended: () => keepOneRecommended,
|
|
59
71
|
missingRequiresFor: () => missingRequiresFor,
|
|
60
72
|
pickActivePromo: () => pickActivePromo,
|
|
73
|
+
previousUtcDay: () => previousUtcDay,
|
|
61
74
|
promoStatus: () => promoStatus,
|
|
75
|
+
readQuotaRecord: () => readQuotaRecord,
|
|
76
|
+
readQuotaValue: () => readQuotaValue,
|
|
62
77
|
resolveBundleAvailability: () => resolveBundleAvailability,
|
|
63
78
|
resolveErrorMessage: () => resolveErrorMessage,
|
|
64
79
|
selectChargeableBundles: () => selectChargeableBundles,
|
|
65
|
-
|
|
80
|
+
settingsSubtreeOf: () => settingsSubtreeOf,
|
|
81
|
+
startOfUtcDay: () => startOfUtcDay,
|
|
82
|
+
toBundleStemRow: () => toBundleStemRow,
|
|
83
|
+
toContractLineItemRecord: () => toContractLineItemRecord,
|
|
84
|
+
toPlanRow: () => toPlanRow,
|
|
85
|
+
toPlanVersionRow: () => toPlanVersionRow,
|
|
86
|
+
toSubscriptionContractRecord: () => toSubscriptionContractRecord
|
|
66
87
|
});
|
|
67
88
|
module.exports = __toCommonJS(index_exports);
|
|
68
89
|
|
|
@@ -70,6 +91,11 @@ module.exports = __toCommonJS(index_exports);
|
|
|
70
91
|
function startOfUtcDay(date) {
|
|
71
92
|
return new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()));
|
|
72
93
|
}
|
|
94
|
+
function previousUtcDay(value) {
|
|
95
|
+
const result = new Date(value);
|
|
96
|
+
result.setUTCDate(result.getUTCDate() - 1);
|
|
97
|
+
return result;
|
|
98
|
+
}
|
|
73
99
|
function buildActivePlanVersionWhere(asOf, options = {}) {
|
|
74
100
|
const asOfDayStart = startOfUtcDay(asOf);
|
|
75
101
|
const validityWindow = [
|
|
@@ -86,6 +112,57 @@ function buildActivePlanVersionWhere(asOf, options = {}) {
|
|
|
86
112
|
}
|
|
87
113
|
var buildActiveVersionWhere = buildActivePlanVersionWhere;
|
|
88
114
|
|
|
115
|
+
// src/bundle-draft-defaults.ts
|
|
116
|
+
function bundleDraftDefaults(data) {
|
|
117
|
+
return {
|
|
118
|
+
baseVersionId: data.baseVersionId ?? null,
|
|
119
|
+
features: data.features,
|
|
120
|
+
quotas: data.quotas ?? {},
|
|
121
|
+
compatibility: data.compatibility ?? {},
|
|
122
|
+
pricingOverrides: data.pricingOverrides ?? [],
|
|
123
|
+
monthlyNet: data.monthlyNet ?? null,
|
|
124
|
+
yearlyNet: data.yearlyNet ?? null,
|
|
125
|
+
marketed: data.marketed ?? true,
|
|
126
|
+
changeNote: data.changeNote ?? "",
|
|
127
|
+
createdByUserId: data.createdByUserId ?? null
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
function bundleStemDefaults(data) {
|
|
131
|
+
return {
|
|
132
|
+
bundleKey: data.bundleKey,
|
|
133
|
+
label: data.label,
|
|
134
|
+
description: data.description ?? null,
|
|
135
|
+
icon: data.icon ?? null,
|
|
136
|
+
sortOrder: data.sortOrder ?? 0,
|
|
137
|
+
i18n: data.i18n ?? {}
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
function toBundleStemRow(row) {
|
|
141
|
+
return {
|
|
142
|
+
id: row.id,
|
|
143
|
+
bundleKey: row.bundleKey,
|
|
144
|
+
label: row.label,
|
|
145
|
+
description: row.description,
|
|
146
|
+
icon: row.icon,
|
|
147
|
+
sortOrder: row.sortOrder,
|
|
148
|
+
i18n: row.i18n !== null && typeof row.i18n === "object" && !Array.isArray(row.i18n) ? row.i18n : {},
|
|
149
|
+
createdAt: row.createdAt.toISOString(),
|
|
150
|
+
updatedAt: row.updatedAt.toISOString(),
|
|
151
|
+
deletedAt: row.deletedAt?.toISOString() ?? null
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
function definedFields(data, keys) {
|
|
155
|
+
const patch = {};
|
|
156
|
+
for (const key of keys) {
|
|
157
|
+
if (data[key] !== void 0) patch[key] = data[key];
|
|
158
|
+
}
|
|
159
|
+
return patch;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// src/catalog-entry.types.ts
|
|
163
|
+
var MARKETING_PRIORITY_MIN = 0;
|
|
164
|
+
var MARKETING_PRIORITY_MAX = 1e4;
|
|
165
|
+
|
|
89
166
|
// src/promotion.types.ts
|
|
90
167
|
function promoStatus(promo, today = /* @__PURE__ */ new Date()) {
|
|
91
168
|
const from = new Date(promo.validFrom);
|
|
@@ -130,25 +207,29 @@ function applyPromo(promo, basePrice) {
|
|
|
130
207
|
return null;
|
|
131
208
|
}
|
|
132
209
|
|
|
210
|
+
// src/quota-value.ts
|
|
211
|
+
function readQuotaValue(value) {
|
|
212
|
+
if (typeof value === "number") return Number.isFinite(value) ? value : null;
|
|
213
|
+
if (typeof value !== "string") return null;
|
|
214
|
+
if (value.trim() === "") return null;
|
|
215
|
+
const parsed = Number(value);
|
|
216
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
217
|
+
}
|
|
218
|
+
var UNCOUNTABLE = -1;
|
|
219
|
+
function readQuotaRecord(value) {
|
|
220
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return {};
|
|
221
|
+
const quotas = {};
|
|
222
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
223
|
+
quotas[key] = readQuotaValue(entry) ?? UNCOUNTABLE;
|
|
224
|
+
}
|
|
225
|
+
return quotas;
|
|
226
|
+
}
|
|
227
|
+
|
|
133
228
|
// src/version-diff.ts
|
|
134
229
|
function classifyPlanDiff(oldV, newV) {
|
|
135
230
|
const changes = [];
|
|
136
231
|
appendFeatureChanges(changes, oldV.features, newV.features);
|
|
137
|
-
|
|
138
|
-
appendNumberChange(
|
|
139
|
-
changes,
|
|
140
|
-
"maxVehicles",
|
|
141
|
-
oldV.maxVehicles,
|
|
142
|
-
newV.maxVehicles,
|
|
143
|
-
"higherIsBetter"
|
|
144
|
-
);
|
|
145
|
-
appendNumberChange(
|
|
146
|
-
changes,
|
|
147
|
-
"maxStorageGb",
|
|
148
|
-
oldV.maxStorageGb,
|
|
149
|
-
newV.maxStorageGb,
|
|
150
|
-
"higherIsBetter"
|
|
151
|
-
);
|
|
232
|
+
appendQuotaChanges(changes, oldV.quotas, newV.quotas);
|
|
152
233
|
appendDecimalChange(changes, "monthlyNet", oldV.monthlyNet, newV.monthlyNet, "lowerIsBetter");
|
|
153
234
|
appendDecimalChange(changes, "yearlyNet", oldV.yearlyNet, newV.yearlyNet, "lowerIsBetter");
|
|
154
235
|
return buildResult(changes);
|
|
@@ -195,11 +276,6 @@ function appendFeatureChanges(out, oldFeatures, newFeatures) {
|
|
|
195
276
|
});
|
|
196
277
|
}
|
|
197
278
|
}
|
|
198
|
-
function appendNumberChange(out, field, oldValue, newValue, polarity) {
|
|
199
|
-
if (oldValue === newValue) return;
|
|
200
|
-
const direction = directionFromCmp(newValue - oldValue, polarity);
|
|
201
|
-
out.push({ field, oldValue, newValue, direction });
|
|
202
|
-
}
|
|
203
279
|
function appendDecimalChange(out, field, oldValue, newValue, polarity) {
|
|
204
280
|
const oldNum = toNumber(oldValue);
|
|
205
281
|
const newNum = toNumber(newValue);
|
|
@@ -230,16 +306,31 @@ function buildResult(changes) {
|
|
|
230
306
|
return { nonRegressive, changes };
|
|
231
307
|
}
|
|
232
308
|
function appendQuotaChanges(out, oldQuotas, newQuotas) {
|
|
309
|
+
const at = (quotas, key) => Object.hasOwn(quotas, key) ? quotas[key] : 0;
|
|
233
310
|
const allKeys = /* @__PURE__ */ new Set([...Object.keys(oldQuotas), ...Object.keys(newQuotas)]);
|
|
234
311
|
for (const key of allKeys) {
|
|
235
|
-
const
|
|
236
|
-
const
|
|
312
|
+
const oldRaw = at(oldQuotas, key);
|
|
313
|
+
const newRaw = at(newQuotas, key);
|
|
314
|
+
if (oldRaw === newRaw) continue;
|
|
315
|
+
const oldValue = toQuotaNumber(oldRaw);
|
|
316
|
+
const newValue = toQuotaNumber(newRaw);
|
|
237
317
|
if (oldValue === newValue) continue;
|
|
238
|
-
|
|
239
|
-
|
|
318
|
+
out.push({
|
|
319
|
+
field: `quotas.${key}`,
|
|
320
|
+
oldValue: reportableQuota(oldRaw, oldValue),
|
|
321
|
+
newValue: reportableQuota(newRaw, newValue),
|
|
322
|
+
direction: directionFromQuotaCmp(oldValue, newValue)
|
|
323
|
+
});
|
|
240
324
|
}
|
|
241
325
|
}
|
|
326
|
+
function toQuotaNumber(value) {
|
|
327
|
+
return readQuotaValue(value) ?? Number.NaN;
|
|
328
|
+
}
|
|
329
|
+
function reportableQuota(raw, asNumber) {
|
|
330
|
+
return Number.isFinite(asNumber) ? asNumber : String(raw);
|
|
331
|
+
}
|
|
242
332
|
function directionFromQuotaCmp(oldValue, newValue) {
|
|
333
|
+
if (!Number.isFinite(oldValue) || !Number.isFinite(newValue)) return "REGRESSION";
|
|
243
334
|
if (oldValue === -1 && newValue !== -1) return "REGRESSION";
|
|
244
335
|
if (newValue === -1 && oldValue !== -1) return "IMPROVEMENT";
|
|
245
336
|
return newValue > oldValue ? "IMPROVEMENT" : "REGRESSION";
|
|
@@ -267,6 +358,57 @@ function appendNullableDecimalChange(out, field, oldValue, newValue, polarity) {
|
|
|
267
358
|
appendDecimalChange(out, field, oldValue, newValue, polarity);
|
|
268
359
|
}
|
|
269
360
|
|
|
361
|
+
// src/settings-subtree.ts
|
|
362
|
+
var CATALOGUE_KEYS = /* @__PURE__ */ new Set([
|
|
363
|
+
"schemaVersion",
|
|
364
|
+
"features",
|
|
365
|
+
"plans"
|
|
366
|
+
]);
|
|
367
|
+
function settingsSubtreeOf(catalog) {
|
|
368
|
+
const settings = {};
|
|
369
|
+
for (const [key, value] of Object.entries(catalog)) {
|
|
370
|
+
if (CATALOGUE_KEYS.has(key)) continue;
|
|
371
|
+
if (value === void 0) continue;
|
|
372
|
+
settings[key] = value;
|
|
373
|
+
}
|
|
374
|
+
return settings;
|
|
375
|
+
}
|
|
376
|
+
function canonicalJson(value) {
|
|
377
|
+
const json = JSON.stringify(value, (_key, member) => {
|
|
378
|
+
if (member && typeof member === "object" && !Array.isArray(member)) {
|
|
379
|
+
const sorted = {};
|
|
380
|
+
for (const name of Object.keys(member).sort()) {
|
|
381
|
+
sorted[name] = member[name];
|
|
382
|
+
}
|
|
383
|
+
return sorted;
|
|
384
|
+
}
|
|
385
|
+
return member;
|
|
386
|
+
});
|
|
387
|
+
return json ?? "undefined";
|
|
388
|
+
}
|
|
389
|
+
function isPlainObject(value) {
|
|
390
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
391
|
+
}
|
|
392
|
+
function diffSettings(before, after) {
|
|
393
|
+
const differences = [];
|
|
394
|
+
collectDifferences(before, after, [], differences);
|
|
395
|
+
return differences.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
|
|
396
|
+
}
|
|
397
|
+
function isObjectOrAbsent(value) {
|
|
398
|
+
return value === void 0 || isPlainObject(value);
|
|
399
|
+
}
|
|
400
|
+
function collectDifferences(before, after, path, out) {
|
|
401
|
+
if (isObjectOrAbsent(before) && isObjectOrAbsent(after) && (before || after)) {
|
|
402
|
+
const names = /* @__PURE__ */ new Set([...Object.keys(before ?? {}), ...Object.keys(after ?? {})]);
|
|
403
|
+
for (const name of names) {
|
|
404
|
+
collectDifferences(before?.[name], after?.[name], [...path, name], out);
|
|
405
|
+
}
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
if (canonicalJson(before) === canonicalJson(after)) return;
|
|
409
|
+
out.push({ path: path.join("."), before, after });
|
|
410
|
+
}
|
|
411
|
+
|
|
270
412
|
// src/ports/persistence-ports.types.ts
|
|
271
413
|
var PersistenceCapabilityError = class extends Error {
|
|
272
414
|
constructor(missing, requiredBy) {
|
|
@@ -337,6 +479,8 @@ var CATALOG_ERROR_CODES = {
|
|
|
337
479
|
BUNDLE_VERSION_SUPERSEDED: "BUNDLE_VERSION_SUPERSEDED",
|
|
338
480
|
BUNDLE_VERSION_REGRESSION: "BUNDLE_VERSION_REGRESSION",
|
|
339
481
|
BUNDLE_VERSION_ZERO_PRICE: "BUNDLE_VERSION_ZERO_PRICE",
|
|
482
|
+
BUNDLE_VERSION_NO_PRICE: "BUNDLE_VERSION_NO_PRICE",
|
|
483
|
+
BUNDLE_VERSION_NOT_PRICED_FOR_PLAN: "BUNDLE_VERSION_NOT_PRICED_FOR_PLAN",
|
|
340
484
|
BUNDLE_VERSION_DISCARD_NOT_IMPLEMENTED: "BUNDLE_VERSION_DISCARD_NOT_IMPLEMENTED",
|
|
341
485
|
BUNDLE_VERSION_VALID_FROM_REQUIRED: "BUNDLE_VERSION_VALID_FROM_REQUIRED",
|
|
342
486
|
BUNDLE_VERSION_VALID_FROM_INVALID: "BUNDLE_VERSION_VALID_FROM_INVALID",
|
|
@@ -366,7 +510,12 @@ var CATALOG_ERROR_CODES = {
|
|
|
366
510
|
// ── discovery ──
|
|
367
511
|
QUOTA_NOT_IN_DISCOVERY_SNAPSHOT: "QUOTA_NOT_IN_DISCOVERY_SNAPSHOT",
|
|
368
512
|
DISCOVERY_STATUS_TRANSITION_INVALID: "DISCOVERY_STATUS_TRANSITION_INVALID",
|
|
369
|
-
DISCOVERY_NOT_INITIALIZED: "DISCOVERY_NOT_INITIALIZED"
|
|
513
|
+
DISCOVERY_NOT_INITIALIZED: "DISCOVERY_NOT_INITIALIZED",
|
|
514
|
+
// ── catalogue import ──
|
|
515
|
+
/** The uploaded document is not a plan catalog — unparseable, or not an object. */
|
|
516
|
+
PLAN_CATALOG_UNREADABLE: "PLAN_CATALOG_UNREADABLE",
|
|
517
|
+
/** It parsed, and then failed the schema or a cross-field rule. */
|
|
518
|
+
PLAN_CATALOG_INVALID: "PLAN_CATALOG_INVALID"
|
|
370
519
|
};
|
|
371
520
|
var BILLING_ERROR_CODES = {
|
|
372
521
|
/**
|
|
@@ -378,6 +527,8 @@ var BILLING_ERROR_CODES = {
|
|
|
378
527
|
BUNDLE_ALREADY_SUBSCRIBED: "BUNDLE_ALREADY_SUBSCRIBED",
|
|
379
528
|
BUNDLE_INCOMPATIBLE_WITH_PLAN: "BUNDLE_INCOMPATIBLE_WITH_PLAN",
|
|
380
529
|
BUNDLE_NOT_SELF_SERVICE: "BUNDLE_NOT_SELF_SERVICE",
|
|
530
|
+
BUNDLE_CYCLE_EXCEEDS_PLAN: "BUNDLE_CYCLE_EXCEEDS_PLAN",
|
|
531
|
+
BUNDLE_NOT_PRICED_FOR_THIS_PLAN: "BUNDLE_NOT_PRICED_FOR_THIS_PLAN",
|
|
381
532
|
SUBSCRIPTION_BUNDLE_ALREADY_CANCELLED: "SUBSCRIPTION_BUNDLE_ALREADY_CANCELLED",
|
|
382
533
|
SUBSCRIPTION_BUNDLE_NOT_CANCELLED: "SUBSCRIPTION_BUNDLE_NOT_CANCELLED",
|
|
383
534
|
SUBSCRIPTION_BUNDLE_CANCELLATION_EFFECTIVE: "SUBSCRIPTION_BUNDLE_CANCELLATION_EFFECTIVE",
|
|
@@ -394,6 +545,84 @@ var BILLING_ERROR_CODES = {
|
|
|
394
545
|
PLAN_NOT_SELF_SERVICE: "PLAN_NOT_SELF_SERVICE",
|
|
395
546
|
/** Plan change refused. Carries `blockers[]` with their own codes. */
|
|
396
547
|
PLAN_CHANGE_BLOCKED: "PLAN_CHANGE_BLOCKED",
|
|
548
|
+
/**
|
|
549
|
+
* The subscription moved between the read a request was decided on and the
|
|
550
|
+
* write it attempted, so nothing was written. The caller reloads and asks
|
|
551
|
+
* again.
|
|
552
|
+
*/
|
|
553
|
+
SUBSCRIPTION_CHANGED: "SUBSCRIPTION_CHANGED",
|
|
554
|
+
/**
|
|
555
|
+
* The tenant has no subscription to act on.
|
|
556
|
+
*
|
|
557
|
+
* `SUBSCRIPTION_NOT_FOUND` states the same fact on the read routes. Both
|
|
558
|
+
* are already on the wire and a code is renamed only deliberately, so both
|
|
559
|
+
* are named here rather than one being dropped behind a consumer's back.
|
|
560
|
+
*/
|
|
561
|
+
NO_SUBSCRIPTION: "NO_SUBSCRIPTION",
|
|
562
|
+
/**
|
|
563
|
+
* The cancellation date the reader was shown is no longer the one the rules
|
|
564
|
+
* return, so the confirmation is refused rather than silently applied.
|
|
565
|
+
* Carries the recomputed dates, so the page can re-ask instead of guessing.
|
|
566
|
+
*/
|
|
567
|
+
CANCELLATION_TERMS_CHANGED: "CANCELLATION_TERMS_CHANGED",
|
|
568
|
+
// ── the preview routes' own blockers and warnings ──
|
|
569
|
+
//
|
|
570
|
+
// These travel inside a 200 response, in `blockers[]` and `warnings[]`, not
|
|
571
|
+
// as thrown errors — but they are read by a person and so belong in the same
|
|
572
|
+
// catalogue. Each carries `params` with the values its sentence names. Two
|
|
573
|
+
// routes emit them: the plan-change preview and the bundle preview.
|
|
574
|
+
//
|
|
575
|
+
// Three of them used to construct their code from the subject:
|
|
576
|
+
// `VEHICLES_OVER_TARGET`, `ENTERPRISE_LOCKED`, `PRO_NOT_SELF_SERVICE`. That
|
|
577
|
+
// made the set grow with every quota and plan an installation defines, so no
|
|
578
|
+
// catalogue could be complete against it and no guard could check one. The
|
|
579
|
+
// subject is a parameter now.
|
|
580
|
+
/** The subscription has ended; its plan can no longer be changed. */
|
|
581
|
+
SUBSCRIPTION_ENDED: "SUBSCRIPTION_ENDED",
|
|
582
|
+
/** An active special contract blocks self-service plan changes. */
|
|
583
|
+
PLAN_LOCKED: "PLAN_LOCKED",
|
|
584
|
+
/** Current usage of one quota exceeds what the target plan allows. */
|
|
585
|
+
QUOTA_OVER_TARGET: "QUOTA_OVER_TARGET",
|
|
586
|
+
/** The change drops features the tenant has today. */
|
|
587
|
+
FEATURE_LOST: "FEATURE_LOST",
|
|
588
|
+
FEATURES_LOST: "FEATURES_LOST",
|
|
589
|
+
/** Target plan and cycle already match what is in place. */
|
|
590
|
+
NO_CHANGE: "NO_CHANGE",
|
|
591
|
+
/** A shorter cycle cannot start inside the term already running. */
|
|
592
|
+
CYCLE_SHORTENS_AT_TERM_END: "CYCLE_SHORTENS_AT_TERM_END",
|
|
593
|
+
/** A cancelled subscription cannot change its billing cycle. */
|
|
594
|
+
CANCELLATION_LOCKS_THE_CYCLE: "CANCELLATION_LOCKS_THE_CYCLE",
|
|
595
|
+
/**
|
|
596
|
+
* A bundle the tenant already holds runs past the cycle they are moving to.
|
|
597
|
+
*
|
|
598
|
+
* Its own code rather than `BUNDLE_CYCLE_EXCEEDS_PLAN`, which states the
|
|
599
|
+
* same rule about a booking that has not been made yet. The two need
|
|
600
|
+
* different sentences: this one can name the day the obstacle lifts and
|
|
601
|
+
* tell the reader to cancel the booking, and that advice is wrong for
|
|
602
|
+
* someone who is only about to book. One template cannot serve both.
|
|
603
|
+
*/
|
|
604
|
+
BUNDLE_BOOKING_OUTLASTS_TARGET_CYCLE: "BUNDLE_BOOKING_OUTLASTS_TARGET_CYCLE",
|
|
605
|
+
/**
|
|
606
|
+
* Features of the previewed bundle are already covered by the plan or by
|
|
607
|
+
* another booked bundle. A warning rather than a blocker: paying twice is
|
|
608
|
+
* the customer's decision, and the preview only has to say so first.
|
|
609
|
+
*/
|
|
610
|
+
REDUNDANT_FEATURES: "REDUNDANT_FEATURES",
|
|
611
|
+
/**
|
|
612
|
+
* A booking's minimum term outlasts the period being cancelled, so the
|
|
613
|
+
* cancellation takes effect at the end of the term, not of the period.
|
|
614
|
+
*/
|
|
615
|
+
MINIMUM_TERM_BINDS: "MINIMUM_TERM_BINDS",
|
|
616
|
+
/**
|
|
617
|
+
* The previewed bundle requires features that neither the plan nor an
|
|
618
|
+
* active booking provides.
|
|
619
|
+
*
|
|
620
|
+
* The same string is a `StrictModeWarningCode` in `bundle.types.ts`, where
|
|
621
|
+
* it names the catalogue-authoring reading of the rule and travels with its
|
|
622
|
+
* own message. This declaration is the booking preview's blocker, which a
|
|
623
|
+
* tenant reads and therefore needs a shipped text for.
|
|
624
|
+
*/
|
|
625
|
+
BUNDLE_FEATURE_DEPENDENCY_UNSATISFIED: "BUNDLE_FEATURE_DEPENDENCY_UNSATISFIED",
|
|
397
626
|
NO_PENDING_PLAN_VERSION: "NO_PENDING_PLAN_VERSION",
|
|
398
627
|
ONBOARDING_CREATE_FAILED: "ONBOARDING_CREATE_FAILED",
|
|
399
628
|
BUNDLE_PREVIEW_ARGUMENT_AMBIGUOUS: "BUNDLE_PREVIEW_ARGUMENT_AMBIGUOUS",
|
|
@@ -417,6 +646,8 @@ var CONTRACT_ERROR_CODES = {
|
|
|
417
646
|
SUBSCRIPTION_CONTRACT_PLAN_LINE_ITEM_REQUIRED: "SUBSCRIPTION_CONTRACT_PLAN_LINE_ITEM_REQUIRED",
|
|
418
647
|
SUBSCRIPTION_CONTRACT_INVALID_DATE: "SUBSCRIPTION_CONTRACT_INVALID_DATE",
|
|
419
648
|
SUBSCRIPTION_CONTRACT_INVALID_WINDOW: "SUBSCRIPTION_CONTRACT_INVALID_WINDOW",
|
|
649
|
+
SUBSCRIPTION_CONTRACT_LINE_ITEM_TAX_MISMATCH: "SUBSCRIPTION_CONTRACT_LINE_ITEM_TAX_MISMATCH",
|
|
650
|
+
SUBSCRIPTION_CONTRACT_LINE_ITEM_CURRENCY_MISMATCH: "SUBSCRIPTION_CONTRACT_LINE_ITEM_CURRENCY_MISMATCH",
|
|
420
651
|
SUBSCRIPTION_CONTRACT_TERMINATION_BEFORE_START: "SUBSCRIPTION_CONTRACT_TERMINATION_BEFORE_START",
|
|
421
652
|
// ── stage 3 ──
|
|
422
653
|
CHECKOUT_OFFER_NOT_FOUND: "CHECKOUT_OFFER_NOT_FOUND",
|
|
@@ -479,6 +710,10 @@ var PROMO_ERROR_CODES = {
|
|
|
479
710
|
PROMO_WOULD_PRODUCE_ZERO_INVOICE: "PROMO_WOULD_PRODUCE_ZERO_INVOICE",
|
|
480
711
|
PROMO_MAX_REDEMPTIONS_LOWERED: "PROMO_MAX_REDEMPTIONS_LOWERED"
|
|
481
712
|
};
|
|
713
|
+
var SETTINGS_ERROR_CODES = {
|
|
714
|
+
/** No recorded change has this id, or the installation keeps no record at all. */
|
|
715
|
+
SETTINGS_CHANGE_NOT_FOUND: "SETTINGS_CHANGE_NOT_FOUND"
|
|
716
|
+
};
|
|
482
717
|
var PLATFORM_ERROR_CODES = {
|
|
483
718
|
...SETUP_ERROR_CODES,
|
|
484
719
|
...AUTH_ERROR_CODES,
|
|
@@ -486,7 +721,8 @@ var PLATFORM_ERROR_CODES = {
|
|
|
486
721
|
...CATALOG_ERROR_CODES,
|
|
487
722
|
...BILLING_ERROR_CODES,
|
|
488
723
|
...CONTRACT_ERROR_CODES,
|
|
489
|
-
...REGISTRATION_ERROR_CODES
|
|
724
|
+
...REGISTRATION_ERROR_CODES,
|
|
725
|
+
...SETTINGS_ERROR_CODES
|
|
490
726
|
};
|
|
491
727
|
|
|
492
728
|
// src/errors.ts
|
|
@@ -584,6 +820,135 @@ var REGISTRATION_STEP_BY_STATUS = {
|
|
|
584
820
|
};
|
|
585
821
|
var REGISTRATION_RESUME_TTL_MINUTES = 60;
|
|
586
822
|
|
|
823
|
+
// src/plan-mapping.ts
|
|
824
|
+
function toPlanRow(row) {
|
|
825
|
+
return {
|
|
826
|
+
id: row.id,
|
|
827
|
+
planKey: row.planKey,
|
|
828
|
+
label: row.label,
|
|
829
|
+
description: row.description,
|
|
830
|
+
icon: row.icon,
|
|
831
|
+
sortOrder: row.sortOrder,
|
|
832
|
+
createdAt: row.createdAt.toISOString(),
|
|
833
|
+
updatedAt: row.updatedAt.toISOString(),
|
|
834
|
+
deletedAt: toIsoOrNull(row.deletedAt)
|
|
835
|
+
};
|
|
836
|
+
}
|
|
837
|
+
function toPlanVersionRow(row, planKey, fields) {
|
|
838
|
+
const mapped = {
|
|
839
|
+
id: row.id,
|
|
840
|
+
version: row.version,
|
|
841
|
+
baseVersionId: row.baseVersionId,
|
|
842
|
+
planId: planKey,
|
|
843
|
+
features: toFeatureKeys(row.features),
|
|
844
|
+
quotas: toQuotaEffects(row.quotas),
|
|
845
|
+
monthlyNet: String(row.monthlyNet),
|
|
846
|
+
yearlyNet: String(row.yearlyNet),
|
|
847
|
+
marketed: row.marketed,
|
|
848
|
+
publishedAt: toIsoOrNull(row.publishedAt),
|
|
849
|
+
supersededAt: toIsoOrNull(row.supersededAt),
|
|
850
|
+
publishedChanges: Array.isArray(row.publishedChanges) ? row.publishedChanges : null,
|
|
851
|
+
changeNote: row.changeNote,
|
|
852
|
+
nonRegressive: row.nonRegressive,
|
|
853
|
+
validFrom: fields.validityWindows ? toIsoOrNull(row.validFrom) : null,
|
|
854
|
+
validUntil: fields.validityWindows ? toIsoOrNull(row.validUntil) : null,
|
|
855
|
+
createdByUserId: row.createdByUserId,
|
|
856
|
+
publishedByUserId: row.publishedByUserId,
|
|
857
|
+
createdAt: row.createdAt.toISOString(),
|
|
858
|
+
updatedAt: row.updatedAt.toISOString()
|
|
859
|
+
};
|
|
860
|
+
if (fields.endsAt) {
|
|
861
|
+
mapped.endsAt = toIsoOrNull(row.endsAt);
|
|
862
|
+
}
|
|
863
|
+
return mapped;
|
|
864
|
+
}
|
|
865
|
+
function toIsoOrNull(value) {
|
|
866
|
+
return value ? value.toISOString() : null;
|
|
867
|
+
}
|
|
868
|
+
function toFeatureKeys(value) {
|
|
869
|
+
if (!Array.isArray(value)) return [];
|
|
870
|
+
return value.filter((entry) => typeof entry === "string");
|
|
871
|
+
}
|
|
872
|
+
function toQuotaEffects(value) {
|
|
873
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
874
|
+
return {};
|
|
875
|
+
}
|
|
876
|
+
const quotas = {};
|
|
877
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
878
|
+
quotas[key] = readQuotaValue(entry) ?? Number.NaN;
|
|
879
|
+
}
|
|
880
|
+
return quotas;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
// src/subscription-contract-mapping.ts
|
|
884
|
+
function toSubscriptionContractRecord(row, lineItems) {
|
|
885
|
+
return {
|
|
886
|
+
id: row.id,
|
|
887
|
+
tenantId: row.tenantId,
|
|
888
|
+
status: row.status,
|
|
889
|
+
effectiveFrom: row.effectiveFrom,
|
|
890
|
+
effectiveUntil: row.effectiveUntil,
|
|
891
|
+
originalOfferId: row.originalOfferId,
|
|
892
|
+
originalPlanVersionId: row.originalPlanVersionId,
|
|
893
|
+
originalBundleVersionIds: toStringArray(row.originalBundleVersionIds),
|
|
894
|
+
entitlementSnapshot: isPlainObject2(row.entitlementSnapshot) ? row.entitlementSnapshot : null,
|
|
895
|
+
priceSnapshot: row.priceSnapshot,
|
|
896
|
+
promotionSnapshots: toUnknownArray(row.promotionSnapshots),
|
|
897
|
+
promoCodeSnapshots: toUnknownArray(row.promoCodeSnapshots),
|
|
898
|
+
termsSnapshot: toRecordOrNull(row.termsSnapshot),
|
|
899
|
+
lineItems: lineItems.map(toContractLineItemRecord),
|
|
900
|
+
createdAt: row.createdAt,
|
|
901
|
+
updatedAt: row.updatedAt
|
|
902
|
+
};
|
|
903
|
+
}
|
|
904
|
+
function toContractLineItemRecord(row) {
|
|
905
|
+
return {
|
|
906
|
+
id: row.id,
|
|
907
|
+
contractId: row.contractId,
|
|
908
|
+
kind: row.kind,
|
|
909
|
+
sourceKey: row.sourceKey,
|
|
910
|
+
sourceVersionId: row.sourceVersionId,
|
|
911
|
+
titleSnapshot: row.titleSnapshot,
|
|
912
|
+
descriptionSnapshot: row.descriptionSnapshot,
|
|
913
|
+
quantity: row.quantity,
|
|
914
|
+
unit: row.unit,
|
|
915
|
+
priceNet: Number(row.priceNet),
|
|
916
|
+
priceGross: Number(row.priceGross),
|
|
917
|
+
billingCycle: row.billingCycle,
|
|
918
|
+
currency: row.currency,
|
|
919
|
+
taxRate: Number(row.taxRate),
|
|
920
|
+
taxAmount: Number(row.taxAmount),
|
|
921
|
+
minimumTermUntil: row.minimumTermUntil,
|
|
922
|
+
featuresSnapshot: toStringArray(row.featuresSnapshot),
|
|
923
|
+
quotaEffectsSnapshot: toQuotaEffects2(row.quotaEffectsSnapshot),
|
|
924
|
+
metadata: toRecordOrNull(row.metadata),
|
|
925
|
+
createdAt: row.createdAt
|
|
926
|
+
};
|
|
927
|
+
}
|
|
928
|
+
function isPlainObject2(value) {
|
|
929
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
930
|
+
}
|
|
931
|
+
function toUnknownArray(value) {
|
|
932
|
+
return Array.isArray(value) ? value : [];
|
|
933
|
+
}
|
|
934
|
+
function toRecordOrNull(value) {
|
|
935
|
+
return isPlainObject2(value) ? value : null;
|
|
936
|
+
}
|
|
937
|
+
function toStringArray(value) {
|
|
938
|
+
if (!Array.isArray(value)) return [];
|
|
939
|
+
return value.filter((entry) => typeof entry === "string");
|
|
940
|
+
}
|
|
941
|
+
function toQuotaEffects2(value) {
|
|
942
|
+
return isPlainObject2(value) ? readQuotaRecord(value) : {};
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
// src/subscription-contract.types.ts
|
|
946
|
+
var BUNDLE_PRICE_LOOKUP_LIMIT = 200;
|
|
947
|
+
var ACTIVE_SUBSCRIPTION_CONTRACT_STATUSES = [
|
|
948
|
+
"active",
|
|
949
|
+
"scheduled"
|
|
950
|
+
];
|
|
951
|
+
|
|
587
952
|
// src/version-editability.ts
|
|
588
953
|
function isVersionEditable(v, now = /* @__PURE__ */ new Date()) {
|
|
589
954
|
if (v.publishedAt === null) {
|
|
@@ -611,6 +976,17 @@ function isVersionEditable(v, now = /* @__PURE__ */ new Date()) {
|
|
|
611
976
|
return { editable: true, reason: "pre-active" };
|
|
612
977
|
}
|
|
613
978
|
|
|
979
|
+
// src/recommended-plan.ts
|
|
980
|
+
function keepOneRecommended(plans, inRequestedLocale) {
|
|
981
|
+
const recommended = plans.filter((plan) => plan.highlight);
|
|
982
|
+
if (recommended.length === 0) return null;
|
|
983
|
+
const winner = recommended.find((plan) => inRequestedLocale.has(plan.planKey)) ?? recommended[0];
|
|
984
|
+
for (const plan of recommended) {
|
|
985
|
+
if (plan !== winner) plan.highlight = false;
|
|
986
|
+
}
|
|
987
|
+
return winner;
|
|
988
|
+
}
|
|
989
|
+
|
|
614
990
|
// src/error-messages.ts
|
|
615
991
|
var ERROR_MESSAGES_EN = {
|
|
616
992
|
// ── setup ──
|
|
@@ -655,6 +1031,8 @@ var ERROR_MESSAGES_EN = {
|
|
|
655
1031
|
BUNDLE_VERSION_SUPERSEDED: "BundleVersion '{bundleVersionId}' has been superseded by a newer version.",
|
|
656
1032
|
BUNDLE_VERSION_REGRESSION: "This bundle version is regressive (feature removed / quota lowered / price raised). Publishing requires an explicit `forceRegressive: true` (UI confirmation dialog with MFA).",
|
|
657
1033
|
BUNDLE_VERSION_ZERO_PRICE: "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.",
|
|
1034
|
+
BUNDLE_VERSION_NO_PRICE: "This bundle version cannot be published without a price: neither a base price nor any plan override resolves one.",
|
|
1035
|
+
BUNDLE_VERSION_NOT_PRICED_FOR_PLAN: "This bundle is offered to plan '{planKey}', which is sold {billingCycle}, and no {billingCycle} price resolves for it \u2014 neither a base price nor a plan override.",
|
|
658
1036
|
BUNDLE_VERSION_DISCARD_NOT_IMPLEMENTED: "Discard is not implemented in the current repository. Implement BundleRepository.deleteDraft.",
|
|
659
1037
|
BUNDLE_VERSION_VALID_FROM_REQUIRED: "Published bundle versions must keep a validFrom. Set a new future date, or edit the draft before publishing.",
|
|
660
1038
|
BUNDLE_VERSION_VALID_FROM_INVALID: "validFrom '{validFrom}' is not a valid date",
|
|
@@ -668,22 +1046,27 @@ var ERROR_MESSAGES_EN = {
|
|
|
668
1046
|
PLAN_VERSION_NOT_FOUND: "PlanVersion '{versionId}' not found",
|
|
669
1047
|
BUNDLE_NOT_FOUND: "Bundle '{bundleId}' not found",
|
|
670
1048
|
BUNDLE_VERSION_NOT_FOUND: "BundleVersion '{bundleVersionId}' not found",
|
|
671
|
-
FEATURE_NOT_FOUND: "Feature '{featureKey}' not found
|
|
672
|
-
QUOTA_NOT_FOUND: "Quota '{quotaKey}' not found
|
|
1049
|
+
FEATURE_NOT_FOUND: "Feature '{featureKey}' not found",
|
|
1050
|
+
QUOTA_NOT_FOUND: "Quota '{quotaKey}' not found",
|
|
673
1051
|
PROMOTION_NOT_FOUND: "Promotion '{promotionId}' not found",
|
|
674
1052
|
MARKETING_PROJECTION_NOT_FOUND: "MarketingProjection '{projectionId}' not found",
|
|
675
|
-
PLAN_ALREADY_EXISTS: "Plan '{planKey}' already exists
|
|
676
|
-
BUNDLE_ALREADY_EXISTS: "Bundle '{bundleKey}' already exists
|
|
1053
|
+
PLAN_ALREADY_EXISTS: "Plan '{planKey}' already exists",
|
|
1054
|
+
BUNDLE_ALREADY_EXISTS: "Bundle '{bundleKey}' already exists",
|
|
677
1055
|
MARKETING_PROJECTION_ALREADY_EXISTS: "Marketing projection for {targetType}/{targetVersionId}/{locale} already exists \u2014 use PATCH to edit it",
|
|
678
1056
|
PLAN_DRAFT_ALREADY_EXISTS: "Plan '{planKey}' already has a draft version v{draftVersion}; publish or discard it first",
|
|
679
1057
|
BUNDLE_DRAFT_ALREADY_EXISTS: "Bundle '{bundleKey}' already has a draft version v{draftVersion}; publish or discard it first",
|
|
680
1058
|
QUOTA_NOT_IN_DISCOVERY_SNAPSHOT: "Quota '{quotaKey}' is not in the discovery snapshot \u2014 a quota that is not declared in code cannot be approved",
|
|
681
1059
|
DISCOVERY_STATUS_TRANSITION_INVALID: "Transition '{from}' \u2192 '{to}' is not allowed",
|
|
682
1060
|
DISCOVERY_NOT_INITIALIZED: "Approval requires a discovery snapshot \u2014 discovery is not initialised (#25)",
|
|
1061
|
+
// ── catalogue import ──
|
|
1062
|
+
PLAN_CATALOG_UNREADABLE: "That file is not a plan catalog \u2014 it could not be read as one. Check that it is the YAML your app declares its plans in.",
|
|
1063
|
+
PLAN_CATALOG_INVALID: "The plan catalog was read, and then rejected: {message}",
|
|
683
1064
|
// ── billing ──
|
|
684
1065
|
BUNDLE_ALREADY_SUBSCRIBED: "Subscription '{subscriptionId}' has already actively booked this bundle.",
|
|
685
1066
|
BUNDLE_INCOMPATIBLE_WITH_PLAN: "BundleVersion '{bundleVersionId}' is not compatible with plan '{planKey}'. Allowed: [{allowedPlanKeys}].",
|
|
686
1067
|
BUNDLE_NOT_SELF_SERVICE: "Bundle '{bundleKey}' is only activated via a special contract. Please contact the contract manager.",
|
|
1068
|
+
BUNDLE_CYCLE_EXCEEDS_PLAN: "A yearly bundle cannot run beside a monthly plan: it would still be committed on every day the plan could end.",
|
|
1069
|
+
BUNDLE_NOT_PRICED_FOR_THIS_PLAN: "This bundle has no {billingCycle} price for the {planKey} plan, so it cannot be booked from here.",
|
|
687
1070
|
SUBSCRIPTION_BUNDLE_ALREADY_CANCELLED: "SubscriptionBundle '{subscriptionBundleId}' is already cancelled.",
|
|
688
1071
|
SUBSCRIPTION_BUNDLE_NOT_CANCELLED: "SubscriptionBundle '{subscriptionBundleId}' is not cancelled.",
|
|
689
1072
|
SUBSCRIPTION_BUNDLE_CANCELLATION_EFFECTIVE: "Cancellation already in effect \u2014 book the bundle again.",
|
|
@@ -692,9 +1075,24 @@ var ERROR_MESSAGES_EN = {
|
|
|
692
1075
|
SUBSCRIPTION_BUNDLE_NOT_FOUND: "SubscriptionBundle '{subscriptionBundleId}' not found",
|
|
693
1076
|
TENANT_NOT_FOUND: "Tenant {slug} not found",
|
|
694
1077
|
NO_ACTIVE_PLAN_VERSION: "No plan version for {planId} active as of {asOf} \u2014 neither the validFrom window is satisfied nor is a latest-live version available.",
|
|
695
|
-
PLAN_NOT_IN_CATALOG: 'Plan "{planKey}" is not in the catalog
|
|
696
|
-
PLAN_NOT_SELF_SERVICE: "{
|
|
1078
|
+
PLAN_NOT_IN_CATALOG: 'Plan "{planKey}" is not in the catalog',
|
|
1079
|
+
PLAN_NOT_SELF_SERVICE: "{planName} is only activated via a special contract. Please contact the contract manager.",
|
|
697
1080
|
PLAN_CHANGE_BLOCKED: "Plan change during onboarding is blocked.",
|
|
1081
|
+
SUBSCRIPTION_CHANGED: "This subscription changed while the request was being decided. Reload it.",
|
|
1082
|
+
NO_SUBSCRIPTION: "This tenant has no subscription to cancel.",
|
|
1083
|
+
CANCELLATION_TERMS_CHANGED: "The effective date changed since it was shown. Confirm the new one.",
|
|
1084
|
+
SUBSCRIPTION_ENDED: "This subscription has ended. Its plan can no longer be changed.",
|
|
1085
|
+
PLAN_LOCKED: "Active {planName} special contract \u2014 please contact the contract manager to change plans.",
|
|
1086
|
+
QUOTA_OVER_TARGET: "Current usage {used} exceeds the target limit {targetMax} ({quotaKey}) in the {planName} plan. Please reduce usage first.",
|
|
1087
|
+
FEATURE_LOST: "Switching means losing access to one feature. Existing data is retained and never deleted \u2014 upgrading again unlocks it.",
|
|
1088
|
+
FEATURES_LOST: "Switching means losing access to {count} features. Existing data is retained and never deleted \u2014 upgrading again unlocks it.",
|
|
1089
|
+
NO_CHANGE: "Target plan and billing cycle already match the current state.",
|
|
1090
|
+
CANCELLATION_LOCKS_THE_CYCLE: "This subscription is cancelled, so its billing cycle cannot change. The plan can \u2014 keep the current cycle to change it today.",
|
|
1091
|
+
CYCLE_SHORTENS_AT_TERM_END: "A monthly {planName} cannot start inside the yearly term you are in. The upgrade takes effect when that term ends; to have it today, keep the yearly cycle.",
|
|
1092
|
+
BUNDLE_BOOKING_OUTLASTS_TARGET_CYCLE: "A yearly bundle is booked until {until}. A monthly plan cannot carry it \u2014 cancel the bundle first, or keep the yearly cycle.",
|
|
1093
|
+
REDUNDANT_FEATURES: "The plan or another booked bundle already includes {count} of the features in this bundle \u2014 booking it pays for them twice.",
|
|
1094
|
+
MINIMUM_TERM_BINDS: "The minimum term extends beyond the end of the period \u2014 the cancellation only takes effect when the minimum term ends.",
|
|
1095
|
+
BUNDLE_FEATURE_DEPENDENCY_UNSATISFIED: "The bundle requires [{features}] \u2014 present neither in the plan nor in the active bundles.",
|
|
698
1096
|
NO_PENDING_PLAN_VERSION: "There is no pending plan version awaiting confirmation.",
|
|
699
1097
|
ONBOARDING_CREATE_FAILED: "The account could not be created. Please try again.",
|
|
700
1098
|
BUNDLE_PREVIEW_ARGUMENT_AMBIGUOUS: "Exactly one of bundleVersionId (add preview) or subscriptionBundleId (cancel preview) must be given.",
|
|
@@ -726,7 +1124,9 @@ var ERROR_MESSAGES_EN = {
|
|
|
726
1124
|
SUBSCRIPTION_CONTRACT_PLAN_LINE_ITEM_REQUIRED: "A subscription contract requires exactly one plan base item.",
|
|
727
1125
|
SUBSCRIPTION_CONTRACT_INVALID_DATE: "{field} must be a valid date.",
|
|
728
1126
|
SUBSCRIPTION_CONTRACT_INVALID_WINDOW: "effectiveUntil must be after effectiveFrom.",
|
|
1127
|
+
SUBSCRIPTION_CONTRACT_LINE_ITEM_TAX_MISMATCH: "A line item's taxAmount must be exactly priceGross minus priceNet.",
|
|
729
1128
|
SUBSCRIPTION_CONTRACT_TERMINATION_BEFORE_START: "effectiveUntil must be after the effectiveFrom of the contract.",
|
|
1129
|
+
SUBSCRIPTION_CONTRACT_LINE_ITEM_CURRENCY_MISMATCH: "A line item must be booked in the currency its contract was priced in.",
|
|
730
1130
|
CHECKOUT_OFFER_NOT_FOUND: "CheckoutOffer '{offerId}' not found",
|
|
731
1131
|
CHECKOUT_OFFER_EXPIRED: "Checkout offer '{offerId}' has expired and cannot be {action}",
|
|
732
1132
|
CHECKOUT_OFFER_ALREADY_CONSUMED: "Checkout offer '{offerId}' has already been consumed and cannot be {action}",
|
|
@@ -750,7 +1150,9 @@ var ERROR_MESSAGES_EN = {
|
|
|
750
1150
|
PLAN_NOT_SELECTED: "Please select a plan first.",
|
|
751
1151
|
MODEL_NOT_AVAILABLE: "This option is not available.",
|
|
752
1152
|
// ── entitlement (code lives in upsell.types.ts) ──
|
|
753
|
-
[FEATURE_NOT_LICENSED]: "Feature {featureKeys} is not included in the current plan."
|
|
1153
|
+
[FEATURE_NOT_LICENSED]: "Feature {featureKeys} is not included in the current plan.",
|
|
1154
|
+
// ── settings ──
|
|
1155
|
+
SETTINGS_CHANGE_NOT_FOUND: "No recorded settings change has this id."
|
|
754
1156
|
};
|
|
755
1157
|
function formatErrorMessage(template, params = {}) {
|
|
756
1158
|
return template.replace(/\{(\w+)\}/g, (match, name) => {
|
|
@@ -812,6 +1214,8 @@ var ERROR_MESSAGES_DE = {
|
|
|
812
1214
|
BUNDLE_VERSION_SUPERSEDED: "BundleVersion '{bundleVersionId}' wurde durch eine neuere Version abgel\xF6st.",
|
|
813
1215
|
BUNDLE_VERSION_REGRESSION: "Diese Bundle-Version ist regressiv (Feature entfernt / Quota gesenkt / Preis erh\xF6ht). Das Ver\xF6ffentlichen erfordert ein ausdr\xFCckliches `forceRegressive: true` (Best\xE4tigungsdialog in der UI mit MFA).",
|
|
814
1216
|
BUNDLE_VERSION_ZERO_PRICE: "Eine Bundle-Version l\xE4sst sich nicht mit einem ausdr\xFCcklichen Preis von 0,00 ver\xF6ffentlichen (Schutz vor Seed-Platzhaltern). Kostenlose Bundles l\xE4sst du auf null, oder du setzt allowZeroPrice.",
|
|
1217
|
+
BUNDLE_VERSION_NO_PRICE: "Diese Bundle-Version kann ohne Preis nicht ver\xF6ffentlicht werden: weder ein Grundpreis noch eine Plan-\xDCberschreibung ergibt einen.",
|
|
1218
|
+
BUNDLE_VERSION_NOT_PRICED_FOR_PLAN: "Dieses Bundle wird dem Plan '{planKey}' angeboten, der {billingCycle} verkauft wird, und daf\xFCr ergibt sich kein {billingCycle} Preis \u2014 weder als Grundpreis noch als Plan-\xDCberschreibung.",
|
|
815
1219
|
BUNDLE_VERSION_DISCARD_NOT_IMPLEMENTED: "Das Verwerfen ist im aktuellen Repository nicht implementiert. Implementiere BundleRepository.deleteDraft.",
|
|
816
1220
|
BUNDLE_VERSION_VALID_FROM_REQUIRED: "Ver\xF6ffentlichte Bundle-Versionen m\xFCssen ein validFrom behalten. Setze ein neues Datum in der Zukunft, oder bearbeite den Entwurf vor dem Ver\xF6ffentlichen.",
|
|
817
1221
|
BUNDLE_VERSION_VALID_FROM_INVALID: "validFrom '{validFrom}' ist kein g\xFCltiges Datum",
|
|
@@ -825,22 +1229,27 @@ var ERROR_MESSAGES_DE = {
|
|
|
825
1229
|
PLAN_VERSION_NOT_FOUND: "PlanVersion '{versionId}' nicht gefunden",
|
|
826
1230
|
BUNDLE_NOT_FOUND: "Bundle '{bundleId}' nicht gefunden",
|
|
827
1231
|
BUNDLE_VERSION_NOT_FOUND: "BundleVersion '{bundleVersionId}' nicht gefunden",
|
|
828
|
-
FEATURE_NOT_FOUND: "Feature '{featureKey}'
|
|
829
|
-
QUOTA_NOT_FOUND: "Quota '{quotaKey}'
|
|
1232
|
+
FEATURE_NOT_FOUND: "Feature '{featureKey}' nicht gefunden",
|
|
1233
|
+
QUOTA_NOT_FOUND: "Quota '{quotaKey}' nicht gefunden",
|
|
830
1234
|
PROMOTION_NOT_FOUND: "Promotion '{promotionId}' nicht gefunden",
|
|
831
1235
|
MARKETING_PROJECTION_NOT_FOUND: "MarketingProjection '{projectionId}' nicht gefunden",
|
|
832
|
-
PLAN_ALREADY_EXISTS: "Plan '{planKey}' gibt es
|
|
833
|
-
BUNDLE_ALREADY_EXISTS: "Bundle '{bundleKey}' gibt es
|
|
1236
|
+
PLAN_ALREADY_EXISTS: "Plan '{planKey}' gibt es bereits",
|
|
1237
|
+
BUNDLE_ALREADY_EXISTS: "Bundle '{bundleKey}' gibt es bereits",
|
|
834
1238
|
MARKETING_PROJECTION_ALREADY_EXISTS: "F\xFCr {targetType}/{targetVersionId}/{locale} gibt es bereits eine Marketing-Projektion \u2014 bearbeite sie per PATCH",
|
|
835
1239
|
PLAN_DRAFT_ALREADY_EXISTS: "Plan '{planKey}' hat bereits die Entwurfsversion v{draftVersion}; ver\xF6ffentliche oder verwirf sie zuerst",
|
|
836
1240
|
BUNDLE_DRAFT_ALREADY_EXISTS: "Bundle '{bundleKey}' hat bereits die Entwurfsversion v{draftVersion}; ver\xF6ffentliche oder verwirf sie zuerst",
|
|
837
1241
|
QUOTA_NOT_IN_DISCOVERY_SNAPSHOT: "Quota '{quotaKey}' steht nicht im Discovery-Snapshot \u2014 eine Quota, die im Code nicht deklariert ist, l\xE4sst sich nicht freigeben",
|
|
838
1242
|
DISCOVERY_STATUS_TRANSITION_INVALID: "Der \xDCbergang '{from}' \u2192 '{to}' ist nicht erlaubt",
|
|
839
1243
|
DISCOVERY_NOT_INITIALIZED: "Die Freigabe braucht einen Discovery-Snapshot \u2014 Discovery ist nicht initialisiert (#25)",
|
|
1244
|
+
// ── catalogue import ──
|
|
1245
|
+
PLAN_CATALOG_UNREADABLE: "Diese Datei ist kein Plankatalog \u2014 sie lie\xDF sich nicht als einer lesen. Pr\xFCfe, ob es die YAML-Datei ist, in der deine App ihre Pl\xE4ne deklariert.",
|
|
1246
|
+
PLAN_CATALOG_INVALID: "Der Plankatalog wurde gelesen und dann abgelehnt: {message}",
|
|
840
1247
|
// ── billing ──
|
|
841
1248
|
BUNDLE_ALREADY_SUBSCRIBED: "Abonnement '{subscriptionId}' hat dieses Bundle bereits aktiv gebucht.",
|
|
842
1249
|
BUNDLE_INCOMPATIBLE_WITH_PLAN: "BundleVersion '{bundleVersionId}' passt nicht zum Plan '{planKey}'. Erlaubt: [{allowedPlanKeys}].",
|
|
843
1250
|
BUNDLE_NOT_SELF_SERVICE: "Bundle '{bundleKey}' wird nur \xFCber einen Sondervertrag freigeschaltet. Bitte wende dich an die Vertragsverwaltung.",
|
|
1251
|
+
BUNDLE_CYCLE_EXCEEDS_PLAN: "Ein j\xE4hrlich abgerechnetes Bundle passt nicht zu einem monatlich abgerechneten Plan: es w\xE4re an jedem Tag gebunden, an dem der Plan enden kann.",
|
|
1252
|
+
BUNDLE_NOT_PRICED_FOR_THIS_PLAN: "F\xFCr dieses Bundle ist im Plan {planKey} kein {billingCycle} Preis hinterlegt, es kann hier deshalb nicht gebucht werden.",
|
|
844
1253
|
SUBSCRIPTION_BUNDLE_ALREADY_CANCELLED: "SubscriptionBundle '{subscriptionBundleId}' ist bereits gek\xFCndigt.",
|
|
845
1254
|
SUBSCRIPTION_BUNDLE_NOT_CANCELLED: "SubscriptionBundle '{subscriptionBundleId}' ist nicht gek\xFCndigt.",
|
|
846
1255
|
SUBSCRIPTION_BUNDLE_CANCELLATION_EFFECTIVE: "Die K\xFCndigung ist bereits wirksam \u2014 buche das Bundle einfach neu.",
|
|
@@ -849,9 +1258,24 @@ var ERROR_MESSAGES_DE = {
|
|
|
849
1258
|
SUBSCRIPTION_BUNDLE_NOT_FOUND: "SubscriptionBundle '{subscriptionBundleId}' nicht gefunden",
|
|
850
1259
|
TENANT_NOT_FOUND: "Mandant {slug} nicht gefunden",
|
|
851
1260
|
NO_ACTIVE_PLAN_VERSION: "Zum {asOf} ist keine Planversion f\xFCr {planId} aktiv \u2014 weder ist das validFrom-Fenster erf\xFCllt, noch steht eine j\xFCngste Live-Version bereit.",
|
|
852
|
-
PLAN_NOT_IN_CATALOG: 'Plan "{planKey}" steht nicht im Katalog
|
|
853
|
-
PLAN_NOT_SELF_SERVICE: "{
|
|
1261
|
+
PLAN_NOT_IN_CATALOG: 'Plan "{planKey}" steht nicht im Katalog',
|
|
1262
|
+
PLAN_NOT_SELF_SERVICE: "{planName} wird nur \xFCber einen Sondervertrag freigeschaltet. Bitte wenden Sie sich an die Vertragsverwaltung.",
|
|
854
1263
|
PLAN_CHANGE_BLOCKED: "W\xE4hrend des Onboardings ist ein Planwechsel gesperrt.",
|
|
1264
|
+
SUBSCRIPTION_CHANGED: "Dieses Abonnement hat sich ge\xE4ndert, w\xE4hrend die Anfrage entschieden wurde. Bitte lade es neu.",
|
|
1265
|
+
NO_SUBSCRIPTION: "Dieser Mandant hat kein Abonnement, das gek\xFCndigt werden kann.",
|
|
1266
|
+
CANCELLATION_TERMS_CHANGED: "Das Wirksamkeitsdatum hat sich seit der Anzeige ge\xE4ndert. Bitte best\xE4tige das neue Datum.",
|
|
1267
|
+
SUBSCRIPTION_ENDED: "Dieses Abonnement ist beendet. Sein Paket kann nicht mehr gewechselt werden.",
|
|
1268
|
+
PLAN_LOCKED: "Aktiver Sondervertrag {planName} \u2014 f\xFCr einen Paketwechsel wenden Sie sich bitte an die Vertragsverwaltung.",
|
|
1269
|
+
QUOTA_OVER_TARGET: "Aktuelle Nutzung {used} \xFCberschreitet das Ziel-Limit {targetMax} ({quotaKey}) im Paket {planName}. Bitte zuerst die Nutzung reduzieren.",
|
|
1270
|
+
FEATURE_LOST: "Beim Wechsel verlieren Sie den Zugriff auf eine Funktion. Vorhandene Daten bleiben erhalten und werden nie gel\xF6scht \u2014 ein sp\xE4teres Upgrade schaltet sie wieder frei.",
|
|
1271
|
+
FEATURES_LOST: "Beim Wechsel verlieren Sie den Zugriff auf {count} Funktionen. Vorhandene Daten bleiben erhalten und werden nie gel\xF6scht \u2014 ein sp\xE4teres Upgrade schaltet sie wieder frei.",
|
|
1272
|
+
NO_CHANGE: "Zielpaket und Abrechnungsrhythmus entsprechen bereits dem aktuellen Stand.",
|
|
1273
|
+
CANCELLATION_LOCKS_THE_CYCLE: "Dieses Abonnement ist gek\xFCndigt, sein Abrechnungsrhythmus kann daher nicht wechseln. Das Paket schon \u2014 behalten Sie den aktuellen Rhythmus, um es heute zu wechseln.",
|
|
1274
|
+
CYCLE_SHORTENS_AT_TERM_END: "Ein monatlich abgerechnetes {planName} kann nicht innerhalb der laufenden Jahreslaufzeit beginnen. Das Upgrade greift zum Ende dieser Laufzeit; wer es sofort m\xF6chte, beh\xE4lt den j\xE4hrlichen Rhythmus.",
|
|
1275
|
+
BUNDLE_BOOKING_OUTLASTS_TARGET_CYCLE: "Ein j\xE4hrlich abgerechnetes Bundle ist bis {until} gebucht. Ein monatlich abgerechnetes Paket kann es nicht tragen \u2014 k\xFCndigen Sie das Bundle zuerst, oder behalten Sie den j\xE4hrlichen Rhythmus.",
|
|
1276
|
+
REDUNDANT_FEATURES: "Der Plan oder ein anderes gebuchtes Bundle enth\xE4lt bereits {count} der Funktionen aus diesem Bundle \u2014 mit der Buchung werden sie doppelt bezahlt.",
|
|
1277
|
+
MINIMUM_TERM_BINDS: "Die Mindestlaufzeit reicht \xFCber das Ende der Periode hinaus \u2014 die K\xFCndigung wird erst zum Ende der Mindestlaufzeit wirksam.",
|
|
1278
|
+
BUNDLE_FEATURE_DEPENDENCY_UNSATISFIED: "Das Bundle setzt [{features}] voraus \u2014 weder im Plan noch in den aktiven Bundles vorhanden.",
|
|
855
1279
|
NO_PENDING_PLAN_VERSION: "Es wartet keine Planversion auf eine Best\xE4tigung.",
|
|
856
1280
|
ONBOARDING_CREATE_FAILED: "Das Konto konnte nicht angelegt werden. Bitte versuche es noch einmal.",
|
|
857
1281
|
BUNDLE_PREVIEW_ARGUMENT_AMBIGUOUS: "Genau eines von bundleVersionId (Vorschau f\xFCrs Buchen) oder subscriptionBundleId (Vorschau f\xFCrs K\xFCndigen) muss angegeben sein.",
|
|
@@ -883,6 +1307,8 @@ var ERROR_MESSAGES_DE = {
|
|
|
883
1307
|
SUBSCRIPTION_CONTRACT_PLAN_LINE_ITEM_REQUIRED: "Ein Abo-Vertrag braucht genau eine Plan-Grundposition.",
|
|
884
1308
|
SUBSCRIPTION_CONTRACT_INVALID_DATE: "{field} muss ein g\xFCltiges Datum sein.",
|
|
885
1309
|
SUBSCRIPTION_CONTRACT_INVALID_WINDOW: "effectiveUntil muss nach effectiveFrom liegen.",
|
|
1310
|
+
SUBSCRIPTION_CONTRACT_LINE_ITEM_TAX_MISMATCH: "taxAmount einer Position muss exakt priceGross minus priceNet sein.",
|
|
1311
|
+
SUBSCRIPTION_CONTRACT_LINE_ITEM_CURRENCY_MISMATCH: "Eine Position muss in der W\xE4hrung gebucht sein, in der ihr Vertrag bepreist ist.",
|
|
886
1312
|
SUBSCRIPTION_CONTRACT_TERMINATION_BEFORE_START: "effectiveUntil muss nach dem effectiveFrom des Vertrags liegen.",
|
|
887
1313
|
CHECKOUT_OFFER_NOT_FOUND: "CheckoutOffer '{offerId}' nicht gefunden",
|
|
888
1314
|
CHECKOUT_OFFER_EXPIRED: "Checkout-Angebot '{offerId}' ist abgelaufen \u2014 '{action}' ist nicht mehr m\xF6glich",
|
|
@@ -907,17 +1333,24 @@ var ERROR_MESSAGES_DE = {
|
|
|
907
1333
|
PLAN_NOT_SELECTED: "Bitte w\xE4hle zuerst einen Plan aus.",
|
|
908
1334
|
MODEL_NOT_AVAILABLE: "Diese Option steht nicht zur Verf\xFCgung.",
|
|
909
1335
|
// ── entitlement (code lives in upsell.types.ts) ──
|
|
910
|
-
[FEATURE_NOT_LICENSED]: "Das Feature {featureKeys} ist im aktuellen Plan nicht enthalten."
|
|
1336
|
+
[FEATURE_NOT_LICENSED]: "Das Feature {featureKeys} ist im aktuellen Plan nicht enthalten.",
|
|
1337
|
+
// ── settings ──
|
|
1338
|
+
SETTINGS_CHANGE_NOT_FOUND: "Keine aufgezeichnete Einstellungs\xE4nderung hat diese ID."
|
|
911
1339
|
};
|
|
912
1340
|
// Annotate the CommonJS export names for ESM import in node:
|
|
913
1341
|
0 && (module.exports = {
|
|
1342
|
+
ACTIVE_SUBSCRIPTION_CONTRACT_STATUSES,
|
|
914
1343
|
AUTH_ERROR_CODES,
|
|
915
1344
|
BILLING_ERROR_CODES,
|
|
1345
|
+
BUNDLE_PRICE_LOOKUP_LIMIT,
|
|
1346
|
+
CATALOGUE_KEYS,
|
|
916
1347
|
CATALOG_ERROR_CODES,
|
|
917
1348
|
CONTRACT_ERROR_CODES,
|
|
918
1349
|
ERROR_MESSAGES_DE,
|
|
919
1350
|
ERROR_MESSAGES_EN,
|
|
920
1351
|
FEATURE_NOT_LICENSED,
|
|
1352
|
+
MARKETING_PRIORITY_MAX,
|
|
1353
|
+
MARKETING_PRIORITY_MIN,
|
|
921
1354
|
OTP_RATE_LIMIT_MAX_SENDS,
|
|
922
1355
|
OTP_RATE_LIMIT_WINDOW_MINUTES,
|
|
923
1356
|
OTP_TTL_MINUTES,
|
|
@@ -933,25 +1366,41 @@ var ERROR_MESSAGES_DE = {
|
|
|
933
1366
|
REGISTRATION_ERROR_CODES,
|
|
934
1367
|
REGISTRATION_RESUME_TTL_MINUTES,
|
|
935
1368
|
REGISTRATION_STEP_BY_STATUS,
|
|
1369
|
+
SETTINGS_ERROR_CODES,
|
|
936
1370
|
SETUP_ERROR_CODES,
|
|
937
1371
|
applyPromo,
|
|
938
1372
|
assertPersistenceCapabilities,
|
|
939
1373
|
buildActivePlanVersionWhere,
|
|
940
1374
|
buildActiveVersionWhere,
|
|
941
1375
|
buildFeatureRequiresIndex,
|
|
1376
|
+
bundleDraftDefaults,
|
|
1377
|
+
bundleStemDefaults,
|
|
1378
|
+
canonicalJson,
|
|
942
1379
|
classifyBundleVersionDiff,
|
|
943
1380
|
classifyPlanDiff,
|
|
944
1381
|
collectUnsatisfiedRequires,
|
|
945
1382
|
coverageExcludingSelf,
|
|
1383
|
+
definedFields,
|
|
1384
|
+
diffSettings,
|
|
946
1385
|
formatErrorMessage,
|
|
947
1386
|
isBundleRedundant,
|
|
948
1387
|
isPlatformUserExistsError,
|
|
949
1388
|
isVersionEditable,
|
|
1389
|
+
keepOneRecommended,
|
|
950
1390
|
missingRequiresFor,
|
|
951
1391
|
pickActivePromo,
|
|
1392
|
+
previousUtcDay,
|
|
952
1393
|
promoStatus,
|
|
1394
|
+
readQuotaRecord,
|
|
1395
|
+
readQuotaValue,
|
|
953
1396
|
resolveBundleAvailability,
|
|
954
1397
|
resolveErrorMessage,
|
|
955
1398
|
selectChargeableBundles,
|
|
956
|
-
|
|
1399
|
+
settingsSubtreeOf,
|
|
1400
|
+
startOfUtcDay,
|
|
1401
|
+
toBundleStemRow,
|
|
1402
|
+
toContractLineItemRecord,
|
|
1403
|
+
toPlanRow,
|
|
1404
|
+
toPlanVersionRow,
|
|
1405
|
+
toSubscriptionContractRecord
|
|
957
1406
|
});
|