@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/dist/index.js CHANGED
@@ -2,6 +2,11 @@
2
2
  function startOfUtcDay(date) {
3
3
  return new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()));
4
4
  }
5
+ function previousUtcDay(value) {
6
+ const result = new Date(value);
7
+ result.setUTCDate(result.getUTCDate() - 1);
8
+ return result;
9
+ }
5
10
  function buildActivePlanVersionWhere(asOf, options = {}) {
6
11
  const asOfDayStart = startOfUtcDay(asOf);
7
12
  const validityWindow = [
@@ -18,6 +23,57 @@ function buildActivePlanVersionWhere(asOf, options = {}) {
18
23
  }
19
24
  var buildActiveVersionWhere = buildActivePlanVersionWhere;
20
25
 
26
+ // src/bundle-draft-defaults.ts
27
+ function bundleDraftDefaults(data) {
28
+ return {
29
+ baseVersionId: data.baseVersionId ?? null,
30
+ features: data.features,
31
+ quotas: data.quotas ?? {},
32
+ compatibility: data.compatibility ?? {},
33
+ pricingOverrides: data.pricingOverrides ?? [],
34
+ monthlyNet: data.monthlyNet ?? null,
35
+ yearlyNet: data.yearlyNet ?? null,
36
+ marketed: data.marketed ?? true,
37
+ changeNote: data.changeNote ?? "",
38
+ createdByUserId: data.createdByUserId ?? null
39
+ };
40
+ }
41
+ function bundleStemDefaults(data) {
42
+ return {
43
+ bundleKey: data.bundleKey,
44
+ label: data.label,
45
+ description: data.description ?? null,
46
+ icon: data.icon ?? null,
47
+ sortOrder: data.sortOrder ?? 0,
48
+ i18n: data.i18n ?? {}
49
+ };
50
+ }
51
+ function toBundleStemRow(row) {
52
+ return {
53
+ id: row.id,
54
+ bundleKey: row.bundleKey,
55
+ label: row.label,
56
+ description: row.description,
57
+ icon: row.icon,
58
+ sortOrder: row.sortOrder,
59
+ i18n: row.i18n !== null && typeof row.i18n === "object" && !Array.isArray(row.i18n) ? row.i18n : {},
60
+ createdAt: row.createdAt.toISOString(),
61
+ updatedAt: row.updatedAt.toISOString(),
62
+ deletedAt: row.deletedAt?.toISOString() ?? null
63
+ };
64
+ }
65
+ function definedFields(data, keys) {
66
+ const patch = {};
67
+ for (const key of keys) {
68
+ if (data[key] !== void 0) patch[key] = data[key];
69
+ }
70
+ return patch;
71
+ }
72
+
73
+ // src/catalog-entry.types.ts
74
+ var MARKETING_PRIORITY_MIN = 0;
75
+ var MARKETING_PRIORITY_MAX = 1e4;
76
+
21
77
  // src/promotion.types.ts
22
78
  function promoStatus(promo, today = /* @__PURE__ */ new Date()) {
23
79
  const from = new Date(promo.validFrom);
@@ -62,25 +118,29 @@ function applyPromo(promo, basePrice) {
62
118
  return null;
63
119
  }
64
120
 
121
+ // src/quota-value.ts
122
+ function readQuotaValue(value) {
123
+ if (typeof value === "number") return Number.isFinite(value) ? value : null;
124
+ if (typeof value !== "string") return null;
125
+ if (value.trim() === "") return null;
126
+ const parsed = Number(value);
127
+ return Number.isFinite(parsed) ? parsed : null;
128
+ }
129
+ var UNCOUNTABLE = -1;
130
+ function readQuotaRecord(value) {
131
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return {};
132
+ const quotas = {};
133
+ for (const [key, entry] of Object.entries(value)) {
134
+ quotas[key] = readQuotaValue(entry) ?? UNCOUNTABLE;
135
+ }
136
+ return quotas;
137
+ }
138
+
65
139
  // src/version-diff.ts
66
140
  function classifyPlanDiff(oldV, newV) {
67
141
  const changes = [];
68
142
  appendFeatureChanges(changes, oldV.features, newV.features);
69
- appendNumberChange(changes, "maxUsers", oldV.maxUsers, newV.maxUsers, "higherIsBetter");
70
- appendNumberChange(
71
- changes,
72
- "maxVehicles",
73
- oldV.maxVehicles,
74
- newV.maxVehicles,
75
- "higherIsBetter"
76
- );
77
- appendNumberChange(
78
- changes,
79
- "maxStorageGb",
80
- oldV.maxStorageGb,
81
- newV.maxStorageGb,
82
- "higherIsBetter"
83
- );
143
+ appendQuotaChanges(changes, oldV.quotas, newV.quotas);
84
144
  appendDecimalChange(changes, "monthlyNet", oldV.monthlyNet, newV.monthlyNet, "lowerIsBetter");
85
145
  appendDecimalChange(changes, "yearlyNet", oldV.yearlyNet, newV.yearlyNet, "lowerIsBetter");
86
146
  return buildResult(changes);
@@ -127,11 +187,6 @@ function appendFeatureChanges(out, oldFeatures, newFeatures) {
127
187
  });
128
188
  }
129
189
  }
130
- function appendNumberChange(out, field, oldValue, newValue, polarity) {
131
- if (oldValue === newValue) return;
132
- const direction = directionFromCmp(newValue - oldValue, polarity);
133
- out.push({ field, oldValue, newValue, direction });
134
- }
135
190
  function appendDecimalChange(out, field, oldValue, newValue, polarity) {
136
191
  const oldNum = toNumber(oldValue);
137
192
  const newNum = toNumber(newValue);
@@ -162,16 +217,31 @@ function buildResult(changes) {
162
217
  return { nonRegressive, changes };
163
218
  }
164
219
  function appendQuotaChanges(out, oldQuotas, newQuotas) {
220
+ const at = (quotas, key) => Object.hasOwn(quotas, key) ? quotas[key] : 0;
165
221
  const allKeys = /* @__PURE__ */ new Set([...Object.keys(oldQuotas), ...Object.keys(newQuotas)]);
166
222
  for (const key of allKeys) {
167
- const oldValue = oldQuotas[key] ?? 0;
168
- const newValue = newQuotas[key] ?? 0;
223
+ const oldRaw = at(oldQuotas, key);
224
+ const newRaw = at(newQuotas, key);
225
+ if (oldRaw === newRaw) continue;
226
+ const oldValue = toQuotaNumber(oldRaw);
227
+ const newValue = toQuotaNumber(newRaw);
169
228
  if (oldValue === newValue) continue;
170
- const direction = directionFromQuotaCmp(oldValue, newValue);
171
- out.push({ field: `quotas.${key}`, oldValue, newValue, direction });
229
+ out.push({
230
+ field: `quotas.${key}`,
231
+ oldValue: reportableQuota(oldRaw, oldValue),
232
+ newValue: reportableQuota(newRaw, newValue),
233
+ direction: directionFromQuotaCmp(oldValue, newValue)
234
+ });
172
235
  }
173
236
  }
237
+ function toQuotaNumber(value) {
238
+ return readQuotaValue(value) ?? Number.NaN;
239
+ }
240
+ function reportableQuota(raw, asNumber) {
241
+ return Number.isFinite(asNumber) ? asNumber : String(raw);
242
+ }
174
243
  function directionFromQuotaCmp(oldValue, newValue) {
244
+ if (!Number.isFinite(oldValue) || !Number.isFinite(newValue)) return "REGRESSION";
175
245
  if (oldValue === -1 && newValue !== -1) return "REGRESSION";
176
246
  if (newValue === -1 && oldValue !== -1) return "IMPROVEMENT";
177
247
  return newValue > oldValue ? "IMPROVEMENT" : "REGRESSION";
@@ -199,6 +269,57 @@ function appendNullableDecimalChange(out, field, oldValue, newValue, polarity) {
199
269
  appendDecimalChange(out, field, oldValue, newValue, polarity);
200
270
  }
201
271
 
272
+ // src/settings-subtree.ts
273
+ var CATALOGUE_KEYS = /* @__PURE__ */ new Set([
274
+ "schemaVersion",
275
+ "features",
276
+ "plans"
277
+ ]);
278
+ function settingsSubtreeOf(catalog) {
279
+ const settings = {};
280
+ for (const [key, value] of Object.entries(catalog)) {
281
+ if (CATALOGUE_KEYS.has(key)) continue;
282
+ if (value === void 0) continue;
283
+ settings[key] = value;
284
+ }
285
+ return settings;
286
+ }
287
+ function canonicalJson(value) {
288
+ const json = JSON.stringify(value, (_key, member) => {
289
+ if (member && typeof member === "object" && !Array.isArray(member)) {
290
+ const sorted = {};
291
+ for (const name of Object.keys(member).sort()) {
292
+ sorted[name] = member[name];
293
+ }
294
+ return sorted;
295
+ }
296
+ return member;
297
+ });
298
+ return json ?? "undefined";
299
+ }
300
+ function isPlainObject(value) {
301
+ return value !== null && typeof value === "object" && !Array.isArray(value);
302
+ }
303
+ function diffSettings(before, after) {
304
+ const differences = [];
305
+ collectDifferences(before, after, [], differences);
306
+ return differences.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
307
+ }
308
+ function isObjectOrAbsent(value) {
309
+ return value === void 0 || isPlainObject(value);
310
+ }
311
+ function collectDifferences(before, after, path, out) {
312
+ if (isObjectOrAbsent(before) && isObjectOrAbsent(after) && (before || after)) {
313
+ const names = /* @__PURE__ */ new Set([...Object.keys(before ?? {}), ...Object.keys(after ?? {})]);
314
+ for (const name of names) {
315
+ collectDifferences(before?.[name], after?.[name], [...path, name], out);
316
+ }
317
+ return;
318
+ }
319
+ if (canonicalJson(before) === canonicalJson(after)) return;
320
+ out.push({ path: path.join("."), before, after });
321
+ }
322
+
202
323
  // src/ports/persistence-ports.types.ts
203
324
  var PersistenceCapabilityError = class extends Error {
204
325
  constructor(missing, requiredBy) {
@@ -269,6 +390,8 @@ var CATALOG_ERROR_CODES = {
269
390
  BUNDLE_VERSION_SUPERSEDED: "BUNDLE_VERSION_SUPERSEDED",
270
391
  BUNDLE_VERSION_REGRESSION: "BUNDLE_VERSION_REGRESSION",
271
392
  BUNDLE_VERSION_ZERO_PRICE: "BUNDLE_VERSION_ZERO_PRICE",
393
+ BUNDLE_VERSION_NO_PRICE: "BUNDLE_VERSION_NO_PRICE",
394
+ BUNDLE_VERSION_NOT_PRICED_FOR_PLAN: "BUNDLE_VERSION_NOT_PRICED_FOR_PLAN",
272
395
  BUNDLE_VERSION_DISCARD_NOT_IMPLEMENTED: "BUNDLE_VERSION_DISCARD_NOT_IMPLEMENTED",
273
396
  BUNDLE_VERSION_VALID_FROM_REQUIRED: "BUNDLE_VERSION_VALID_FROM_REQUIRED",
274
397
  BUNDLE_VERSION_VALID_FROM_INVALID: "BUNDLE_VERSION_VALID_FROM_INVALID",
@@ -298,7 +421,12 @@ var CATALOG_ERROR_CODES = {
298
421
  // ── discovery ──
299
422
  QUOTA_NOT_IN_DISCOVERY_SNAPSHOT: "QUOTA_NOT_IN_DISCOVERY_SNAPSHOT",
300
423
  DISCOVERY_STATUS_TRANSITION_INVALID: "DISCOVERY_STATUS_TRANSITION_INVALID",
301
- DISCOVERY_NOT_INITIALIZED: "DISCOVERY_NOT_INITIALIZED"
424
+ DISCOVERY_NOT_INITIALIZED: "DISCOVERY_NOT_INITIALIZED",
425
+ // ── catalogue import ──
426
+ /** The uploaded document is not a plan catalog — unparseable, or not an object. */
427
+ PLAN_CATALOG_UNREADABLE: "PLAN_CATALOG_UNREADABLE",
428
+ /** It parsed, and then failed the schema or a cross-field rule. */
429
+ PLAN_CATALOG_INVALID: "PLAN_CATALOG_INVALID"
302
430
  };
303
431
  var BILLING_ERROR_CODES = {
304
432
  /**
@@ -310,6 +438,8 @@ var BILLING_ERROR_CODES = {
310
438
  BUNDLE_ALREADY_SUBSCRIBED: "BUNDLE_ALREADY_SUBSCRIBED",
311
439
  BUNDLE_INCOMPATIBLE_WITH_PLAN: "BUNDLE_INCOMPATIBLE_WITH_PLAN",
312
440
  BUNDLE_NOT_SELF_SERVICE: "BUNDLE_NOT_SELF_SERVICE",
441
+ BUNDLE_CYCLE_EXCEEDS_PLAN: "BUNDLE_CYCLE_EXCEEDS_PLAN",
442
+ BUNDLE_NOT_PRICED_FOR_THIS_PLAN: "BUNDLE_NOT_PRICED_FOR_THIS_PLAN",
313
443
  SUBSCRIPTION_BUNDLE_ALREADY_CANCELLED: "SUBSCRIPTION_BUNDLE_ALREADY_CANCELLED",
314
444
  SUBSCRIPTION_BUNDLE_NOT_CANCELLED: "SUBSCRIPTION_BUNDLE_NOT_CANCELLED",
315
445
  SUBSCRIPTION_BUNDLE_CANCELLATION_EFFECTIVE: "SUBSCRIPTION_BUNDLE_CANCELLATION_EFFECTIVE",
@@ -326,6 +456,84 @@ var BILLING_ERROR_CODES = {
326
456
  PLAN_NOT_SELF_SERVICE: "PLAN_NOT_SELF_SERVICE",
327
457
  /** Plan change refused. Carries `blockers[]` with their own codes. */
328
458
  PLAN_CHANGE_BLOCKED: "PLAN_CHANGE_BLOCKED",
459
+ /**
460
+ * The subscription moved between the read a request was decided on and the
461
+ * write it attempted, so nothing was written. The caller reloads and asks
462
+ * again.
463
+ */
464
+ SUBSCRIPTION_CHANGED: "SUBSCRIPTION_CHANGED",
465
+ /**
466
+ * The tenant has no subscription to act on.
467
+ *
468
+ * `SUBSCRIPTION_NOT_FOUND` states the same fact on the read routes. Both
469
+ * are already on the wire and a code is renamed only deliberately, so both
470
+ * are named here rather than one being dropped behind a consumer's back.
471
+ */
472
+ NO_SUBSCRIPTION: "NO_SUBSCRIPTION",
473
+ /**
474
+ * The cancellation date the reader was shown is no longer the one the rules
475
+ * return, so the confirmation is refused rather than silently applied.
476
+ * Carries the recomputed dates, so the page can re-ask instead of guessing.
477
+ */
478
+ CANCELLATION_TERMS_CHANGED: "CANCELLATION_TERMS_CHANGED",
479
+ // ── the preview routes' own blockers and warnings ──
480
+ //
481
+ // These travel inside a 200 response, in `blockers[]` and `warnings[]`, not
482
+ // as thrown errors — but they are read by a person and so belong in the same
483
+ // catalogue. Each carries `params` with the values its sentence names. Two
484
+ // routes emit them: the plan-change preview and the bundle preview.
485
+ //
486
+ // Three of them used to construct their code from the subject:
487
+ // `VEHICLES_OVER_TARGET`, `ENTERPRISE_LOCKED`, `PRO_NOT_SELF_SERVICE`. That
488
+ // made the set grow with every quota and plan an installation defines, so no
489
+ // catalogue could be complete against it and no guard could check one. The
490
+ // subject is a parameter now.
491
+ /** The subscription has ended; its plan can no longer be changed. */
492
+ SUBSCRIPTION_ENDED: "SUBSCRIPTION_ENDED",
493
+ /** An active special contract blocks self-service plan changes. */
494
+ PLAN_LOCKED: "PLAN_LOCKED",
495
+ /** Current usage of one quota exceeds what the target plan allows. */
496
+ QUOTA_OVER_TARGET: "QUOTA_OVER_TARGET",
497
+ /** The change drops features the tenant has today. */
498
+ FEATURE_LOST: "FEATURE_LOST",
499
+ FEATURES_LOST: "FEATURES_LOST",
500
+ /** Target plan and cycle already match what is in place. */
501
+ NO_CHANGE: "NO_CHANGE",
502
+ /** A shorter cycle cannot start inside the term already running. */
503
+ CYCLE_SHORTENS_AT_TERM_END: "CYCLE_SHORTENS_AT_TERM_END",
504
+ /** A cancelled subscription cannot change its billing cycle. */
505
+ CANCELLATION_LOCKS_THE_CYCLE: "CANCELLATION_LOCKS_THE_CYCLE",
506
+ /**
507
+ * A bundle the tenant already holds runs past the cycle they are moving to.
508
+ *
509
+ * Its own code rather than `BUNDLE_CYCLE_EXCEEDS_PLAN`, which states the
510
+ * same rule about a booking that has not been made yet. The two need
511
+ * different sentences: this one can name the day the obstacle lifts and
512
+ * tell the reader to cancel the booking, and that advice is wrong for
513
+ * someone who is only about to book. One template cannot serve both.
514
+ */
515
+ BUNDLE_BOOKING_OUTLASTS_TARGET_CYCLE: "BUNDLE_BOOKING_OUTLASTS_TARGET_CYCLE",
516
+ /**
517
+ * Features of the previewed bundle are already covered by the plan or by
518
+ * another booked bundle. A warning rather than a blocker: paying twice is
519
+ * the customer's decision, and the preview only has to say so first.
520
+ */
521
+ REDUNDANT_FEATURES: "REDUNDANT_FEATURES",
522
+ /**
523
+ * A booking's minimum term outlasts the period being cancelled, so the
524
+ * cancellation takes effect at the end of the term, not of the period.
525
+ */
526
+ MINIMUM_TERM_BINDS: "MINIMUM_TERM_BINDS",
527
+ /**
528
+ * The previewed bundle requires features that neither the plan nor an
529
+ * active booking provides.
530
+ *
531
+ * The same string is a `StrictModeWarningCode` in `bundle.types.ts`, where
532
+ * it names the catalogue-authoring reading of the rule and travels with its
533
+ * own message. This declaration is the booking preview's blocker, which a
534
+ * tenant reads and therefore needs a shipped text for.
535
+ */
536
+ BUNDLE_FEATURE_DEPENDENCY_UNSATISFIED: "BUNDLE_FEATURE_DEPENDENCY_UNSATISFIED",
329
537
  NO_PENDING_PLAN_VERSION: "NO_PENDING_PLAN_VERSION",
330
538
  ONBOARDING_CREATE_FAILED: "ONBOARDING_CREATE_FAILED",
331
539
  BUNDLE_PREVIEW_ARGUMENT_AMBIGUOUS: "BUNDLE_PREVIEW_ARGUMENT_AMBIGUOUS",
@@ -349,6 +557,8 @@ var CONTRACT_ERROR_CODES = {
349
557
  SUBSCRIPTION_CONTRACT_PLAN_LINE_ITEM_REQUIRED: "SUBSCRIPTION_CONTRACT_PLAN_LINE_ITEM_REQUIRED",
350
558
  SUBSCRIPTION_CONTRACT_INVALID_DATE: "SUBSCRIPTION_CONTRACT_INVALID_DATE",
351
559
  SUBSCRIPTION_CONTRACT_INVALID_WINDOW: "SUBSCRIPTION_CONTRACT_INVALID_WINDOW",
560
+ SUBSCRIPTION_CONTRACT_LINE_ITEM_TAX_MISMATCH: "SUBSCRIPTION_CONTRACT_LINE_ITEM_TAX_MISMATCH",
561
+ SUBSCRIPTION_CONTRACT_LINE_ITEM_CURRENCY_MISMATCH: "SUBSCRIPTION_CONTRACT_LINE_ITEM_CURRENCY_MISMATCH",
352
562
  SUBSCRIPTION_CONTRACT_TERMINATION_BEFORE_START: "SUBSCRIPTION_CONTRACT_TERMINATION_BEFORE_START",
353
563
  // ── stage 3 ──
354
564
  CHECKOUT_OFFER_NOT_FOUND: "CHECKOUT_OFFER_NOT_FOUND",
@@ -411,6 +621,10 @@ var PROMO_ERROR_CODES = {
411
621
  PROMO_WOULD_PRODUCE_ZERO_INVOICE: "PROMO_WOULD_PRODUCE_ZERO_INVOICE",
412
622
  PROMO_MAX_REDEMPTIONS_LOWERED: "PROMO_MAX_REDEMPTIONS_LOWERED"
413
623
  };
624
+ var SETTINGS_ERROR_CODES = {
625
+ /** No recorded change has this id, or the installation keeps no record at all. */
626
+ SETTINGS_CHANGE_NOT_FOUND: "SETTINGS_CHANGE_NOT_FOUND"
627
+ };
414
628
  var PLATFORM_ERROR_CODES = {
415
629
  ...SETUP_ERROR_CODES,
416
630
  ...AUTH_ERROR_CODES,
@@ -418,7 +632,8 @@ var PLATFORM_ERROR_CODES = {
418
632
  ...CATALOG_ERROR_CODES,
419
633
  ...BILLING_ERROR_CODES,
420
634
  ...CONTRACT_ERROR_CODES,
421
- ...REGISTRATION_ERROR_CODES
635
+ ...REGISTRATION_ERROR_CODES,
636
+ ...SETTINGS_ERROR_CODES
422
637
  };
423
638
 
424
639
  // src/errors.ts
@@ -516,6 +731,135 @@ var REGISTRATION_STEP_BY_STATUS = {
516
731
  };
517
732
  var REGISTRATION_RESUME_TTL_MINUTES = 60;
518
733
 
734
+ // src/plan-mapping.ts
735
+ function toPlanRow(row) {
736
+ return {
737
+ id: row.id,
738
+ planKey: row.planKey,
739
+ label: row.label,
740
+ description: row.description,
741
+ icon: row.icon,
742
+ sortOrder: row.sortOrder,
743
+ createdAt: row.createdAt.toISOString(),
744
+ updatedAt: row.updatedAt.toISOString(),
745
+ deletedAt: toIsoOrNull(row.deletedAt)
746
+ };
747
+ }
748
+ function toPlanVersionRow(row, planKey, fields) {
749
+ const mapped = {
750
+ id: row.id,
751
+ version: row.version,
752
+ baseVersionId: row.baseVersionId,
753
+ planId: planKey,
754
+ features: toFeatureKeys(row.features),
755
+ quotas: toQuotaEffects(row.quotas),
756
+ monthlyNet: String(row.monthlyNet),
757
+ yearlyNet: String(row.yearlyNet),
758
+ marketed: row.marketed,
759
+ publishedAt: toIsoOrNull(row.publishedAt),
760
+ supersededAt: toIsoOrNull(row.supersededAt),
761
+ publishedChanges: Array.isArray(row.publishedChanges) ? row.publishedChanges : null,
762
+ changeNote: row.changeNote,
763
+ nonRegressive: row.nonRegressive,
764
+ validFrom: fields.validityWindows ? toIsoOrNull(row.validFrom) : null,
765
+ validUntil: fields.validityWindows ? toIsoOrNull(row.validUntil) : null,
766
+ createdByUserId: row.createdByUserId,
767
+ publishedByUserId: row.publishedByUserId,
768
+ createdAt: row.createdAt.toISOString(),
769
+ updatedAt: row.updatedAt.toISOString()
770
+ };
771
+ if (fields.endsAt) {
772
+ mapped.endsAt = toIsoOrNull(row.endsAt);
773
+ }
774
+ return mapped;
775
+ }
776
+ function toIsoOrNull(value) {
777
+ return value ? value.toISOString() : null;
778
+ }
779
+ function toFeatureKeys(value) {
780
+ if (!Array.isArray(value)) return [];
781
+ return value.filter((entry) => typeof entry === "string");
782
+ }
783
+ function toQuotaEffects(value) {
784
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
785
+ return {};
786
+ }
787
+ const quotas = {};
788
+ for (const [key, entry] of Object.entries(value)) {
789
+ quotas[key] = readQuotaValue(entry) ?? Number.NaN;
790
+ }
791
+ return quotas;
792
+ }
793
+
794
+ // src/subscription-contract-mapping.ts
795
+ function toSubscriptionContractRecord(row, lineItems) {
796
+ return {
797
+ id: row.id,
798
+ tenantId: row.tenantId,
799
+ status: row.status,
800
+ effectiveFrom: row.effectiveFrom,
801
+ effectiveUntil: row.effectiveUntil,
802
+ originalOfferId: row.originalOfferId,
803
+ originalPlanVersionId: row.originalPlanVersionId,
804
+ originalBundleVersionIds: toStringArray(row.originalBundleVersionIds),
805
+ entitlementSnapshot: isPlainObject2(row.entitlementSnapshot) ? row.entitlementSnapshot : null,
806
+ priceSnapshot: row.priceSnapshot,
807
+ promotionSnapshots: toUnknownArray(row.promotionSnapshots),
808
+ promoCodeSnapshots: toUnknownArray(row.promoCodeSnapshots),
809
+ termsSnapshot: toRecordOrNull(row.termsSnapshot),
810
+ lineItems: lineItems.map(toContractLineItemRecord),
811
+ createdAt: row.createdAt,
812
+ updatedAt: row.updatedAt
813
+ };
814
+ }
815
+ function toContractLineItemRecord(row) {
816
+ return {
817
+ id: row.id,
818
+ contractId: row.contractId,
819
+ kind: row.kind,
820
+ sourceKey: row.sourceKey,
821
+ sourceVersionId: row.sourceVersionId,
822
+ titleSnapshot: row.titleSnapshot,
823
+ descriptionSnapshot: row.descriptionSnapshot,
824
+ quantity: row.quantity,
825
+ unit: row.unit,
826
+ priceNet: Number(row.priceNet),
827
+ priceGross: Number(row.priceGross),
828
+ billingCycle: row.billingCycle,
829
+ currency: row.currency,
830
+ taxRate: Number(row.taxRate),
831
+ taxAmount: Number(row.taxAmount),
832
+ minimumTermUntil: row.minimumTermUntil,
833
+ featuresSnapshot: toStringArray(row.featuresSnapshot),
834
+ quotaEffectsSnapshot: toQuotaEffects2(row.quotaEffectsSnapshot),
835
+ metadata: toRecordOrNull(row.metadata),
836
+ createdAt: row.createdAt
837
+ };
838
+ }
839
+ function isPlainObject2(value) {
840
+ return value !== null && typeof value === "object" && !Array.isArray(value);
841
+ }
842
+ function toUnknownArray(value) {
843
+ return Array.isArray(value) ? value : [];
844
+ }
845
+ function toRecordOrNull(value) {
846
+ return isPlainObject2(value) ? value : null;
847
+ }
848
+ function toStringArray(value) {
849
+ if (!Array.isArray(value)) return [];
850
+ return value.filter((entry) => typeof entry === "string");
851
+ }
852
+ function toQuotaEffects2(value) {
853
+ return isPlainObject2(value) ? readQuotaRecord(value) : {};
854
+ }
855
+
856
+ // src/subscription-contract.types.ts
857
+ var BUNDLE_PRICE_LOOKUP_LIMIT = 200;
858
+ var ACTIVE_SUBSCRIPTION_CONTRACT_STATUSES = [
859
+ "active",
860
+ "scheduled"
861
+ ];
862
+
519
863
  // src/version-editability.ts
520
864
  function isVersionEditable(v, now = /* @__PURE__ */ new Date()) {
521
865
  if (v.publishedAt === null) {
@@ -543,6 +887,17 @@ function isVersionEditable(v, now = /* @__PURE__ */ new Date()) {
543
887
  return { editable: true, reason: "pre-active" };
544
888
  }
545
889
 
890
+ // src/recommended-plan.ts
891
+ function keepOneRecommended(plans, inRequestedLocale) {
892
+ const recommended = plans.filter((plan) => plan.highlight);
893
+ if (recommended.length === 0) return null;
894
+ const winner = recommended.find((plan) => inRequestedLocale.has(plan.planKey)) ?? recommended[0];
895
+ for (const plan of recommended) {
896
+ if (plan !== winner) plan.highlight = false;
897
+ }
898
+ return winner;
899
+ }
900
+
546
901
  // src/error-messages.ts
547
902
  var ERROR_MESSAGES_EN = {
548
903
  // ── setup ──
@@ -587,6 +942,8 @@ var ERROR_MESSAGES_EN = {
587
942
  BUNDLE_VERSION_SUPERSEDED: "BundleVersion '{bundleVersionId}' has been superseded by a newer version.",
588
943
  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).",
589
944
  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.",
945
+ BUNDLE_VERSION_NO_PRICE: "This bundle version cannot be published without a price: neither a base price nor any plan override resolves one.",
946
+ 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.",
590
947
  BUNDLE_VERSION_DISCARD_NOT_IMPLEMENTED: "Discard is not implemented in the current repository. Implement BundleRepository.deleteDraft.",
591
948
  BUNDLE_VERSION_VALID_FROM_REQUIRED: "Published bundle versions must keep a validFrom. Set a new future date, or edit the draft before publishing.",
592
949
  BUNDLE_VERSION_VALID_FROM_INVALID: "validFrom '{validFrom}' is not a valid date",
@@ -600,22 +957,27 @@ var ERROR_MESSAGES_EN = {
600
957
  PLAN_VERSION_NOT_FOUND: "PlanVersion '{versionId}' not found",
601
958
  BUNDLE_NOT_FOUND: "Bundle '{bundleId}' not found",
602
959
  BUNDLE_VERSION_NOT_FOUND: "BundleVersion '{bundleVersionId}' not found",
603
- FEATURE_NOT_FOUND: "Feature '{featureKey}' not found in project '{projectKey}'",
604
- QUOTA_NOT_FOUND: "Quota '{quotaKey}' not found in project '{projectKey}'",
960
+ FEATURE_NOT_FOUND: "Feature '{featureKey}' not found",
961
+ QUOTA_NOT_FOUND: "Quota '{quotaKey}' not found",
605
962
  PROMOTION_NOT_FOUND: "Promotion '{promotionId}' not found",
606
963
  MARKETING_PROJECTION_NOT_FOUND: "MarketingProjection '{projectionId}' not found",
607
- PLAN_ALREADY_EXISTS: "Plan '{planKey}' already exists in project '{projectKey}'",
608
- BUNDLE_ALREADY_EXISTS: "Bundle '{bundleKey}' already exists in project '{projectKey}'",
964
+ PLAN_ALREADY_EXISTS: "Plan '{planKey}' already exists",
965
+ BUNDLE_ALREADY_EXISTS: "Bundle '{bundleKey}' already exists",
609
966
  MARKETING_PROJECTION_ALREADY_EXISTS: "Marketing projection for {targetType}/{targetVersionId}/{locale} already exists \u2014 use PATCH to edit it",
610
967
  PLAN_DRAFT_ALREADY_EXISTS: "Plan '{planKey}' already has a draft version v{draftVersion}; publish or discard it first",
611
968
  BUNDLE_DRAFT_ALREADY_EXISTS: "Bundle '{bundleKey}' already has a draft version v{draftVersion}; publish or discard it first",
612
969
  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",
613
970
  DISCOVERY_STATUS_TRANSITION_INVALID: "Transition '{from}' \u2192 '{to}' is not allowed",
614
971
  DISCOVERY_NOT_INITIALIZED: "Approval requires a discovery snapshot \u2014 discovery is not initialised (#25)",
972
+ // ── catalogue import ──
973
+ 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.",
974
+ PLAN_CATALOG_INVALID: "The plan catalog was read, and then rejected: {message}",
615
975
  // ── billing ──
616
976
  BUNDLE_ALREADY_SUBSCRIBED: "Subscription '{subscriptionId}' has already actively booked this bundle.",
617
977
  BUNDLE_INCOMPATIBLE_WITH_PLAN: "BundleVersion '{bundleVersionId}' is not compatible with plan '{planKey}'. Allowed: [{allowedPlanKeys}].",
618
978
  BUNDLE_NOT_SELF_SERVICE: "Bundle '{bundleKey}' is only activated via a special contract. Please contact the contract manager.",
979
+ 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.",
980
+ BUNDLE_NOT_PRICED_FOR_THIS_PLAN: "This bundle has no {billingCycle} price for the {planKey} plan, so it cannot be booked from here.",
619
981
  SUBSCRIPTION_BUNDLE_ALREADY_CANCELLED: "SubscriptionBundle '{subscriptionBundleId}' is already cancelled.",
620
982
  SUBSCRIPTION_BUNDLE_NOT_CANCELLED: "SubscriptionBundle '{subscriptionBundleId}' is not cancelled.",
621
983
  SUBSCRIPTION_BUNDLE_CANCELLATION_EFFECTIVE: "Cancellation already in effect \u2014 book the bundle again.",
@@ -624,9 +986,24 @@ var ERROR_MESSAGES_EN = {
624
986
  SUBSCRIPTION_BUNDLE_NOT_FOUND: "SubscriptionBundle '{subscriptionBundleId}' not found",
625
987
  TENANT_NOT_FOUND: "Tenant {slug} not found",
626
988
  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.",
627
- PLAN_NOT_IN_CATALOG: 'Plan "{planKey}" is not in the catalog ({projectKey})',
628
- PLAN_NOT_SELF_SERVICE: "{planKey} is not activated via self-service.",
989
+ PLAN_NOT_IN_CATALOG: 'Plan "{planKey}" is not in the catalog',
990
+ PLAN_NOT_SELF_SERVICE: "{planName} is only activated via a special contract. Please contact the contract manager.",
629
991
  PLAN_CHANGE_BLOCKED: "Plan change during onboarding is blocked.",
992
+ SUBSCRIPTION_CHANGED: "This subscription changed while the request was being decided. Reload it.",
993
+ NO_SUBSCRIPTION: "This tenant has no subscription to cancel.",
994
+ CANCELLATION_TERMS_CHANGED: "The effective date changed since it was shown. Confirm the new one.",
995
+ SUBSCRIPTION_ENDED: "This subscription has ended. Its plan can no longer be changed.",
996
+ PLAN_LOCKED: "Active {planName} special contract \u2014 please contact the contract manager to change plans.",
997
+ QUOTA_OVER_TARGET: "Current usage {used} exceeds the target limit {targetMax} ({quotaKey}) in the {planName} plan. Please reduce usage first.",
998
+ FEATURE_LOST: "Switching means losing access to one feature. Existing data is retained and never deleted \u2014 upgrading again unlocks it.",
999
+ FEATURES_LOST: "Switching means losing access to {count} features. Existing data is retained and never deleted \u2014 upgrading again unlocks it.",
1000
+ NO_CHANGE: "Target plan and billing cycle already match the current state.",
1001
+ 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.",
1002
+ 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.",
1003
+ 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.",
1004
+ REDUNDANT_FEATURES: "The plan or another booked bundle already includes {count} of the features in this bundle \u2014 booking it pays for them twice.",
1005
+ MINIMUM_TERM_BINDS: "The minimum term extends beyond the end of the period \u2014 the cancellation only takes effect when the minimum term ends.",
1006
+ BUNDLE_FEATURE_DEPENDENCY_UNSATISFIED: "The bundle requires [{features}] \u2014 present neither in the plan nor in the active bundles.",
630
1007
  NO_PENDING_PLAN_VERSION: "There is no pending plan version awaiting confirmation.",
631
1008
  ONBOARDING_CREATE_FAILED: "The account could not be created. Please try again.",
632
1009
  BUNDLE_PREVIEW_ARGUMENT_AMBIGUOUS: "Exactly one of bundleVersionId (add preview) or subscriptionBundleId (cancel preview) must be given.",
@@ -658,7 +1035,9 @@ var ERROR_MESSAGES_EN = {
658
1035
  SUBSCRIPTION_CONTRACT_PLAN_LINE_ITEM_REQUIRED: "A subscription contract requires exactly one plan base item.",
659
1036
  SUBSCRIPTION_CONTRACT_INVALID_DATE: "{field} must be a valid date.",
660
1037
  SUBSCRIPTION_CONTRACT_INVALID_WINDOW: "effectiveUntil must be after effectiveFrom.",
1038
+ SUBSCRIPTION_CONTRACT_LINE_ITEM_TAX_MISMATCH: "A line item's taxAmount must be exactly priceGross minus priceNet.",
661
1039
  SUBSCRIPTION_CONTRACT_TERMINATION_BEFORE_START: "effectiveUntil must be after the effectiveFrom of the contract.",
1040
+ SUBSCRIPTION_CONTRACT_LINE_ITEM_CURRENCY_MISMATCH: "A line item must be booked in the currency its contract was priced in.",
662
1041
  CHECKOUT_OFFER_NOT_FOUND: "CheckoutOffer '{offerId}' not found",
663
1042
  CHECKOUT_OFFER_EXPIRED: "Checkout offer '{offerId}' has expired and cannot be {action}",
664
1043
  CHECKOUT_OFFER_ALREADY_CONSUMED: "Checkout offer '{offerId}' has already been consumed and cannot be {action}",
@@ -682,7 +1061,9 @@ var ERROR_MESSAGES_EN = {
682
1061
  PLAN_NOT_SELECTED: "Please select a plan first.",
683
1062
  MODEL_NOT_AVAILABLE: "This option is not available.",
684
1063
  // ── entitlement (code lives in upsell.types.ts) ──
685
- [FEATURE_NOT_LICENSED]: "Feature {featureKeys} is not included in the current plan."
1064
+ [FEATURE_NOT_LICENSED]: "Feature {featureKeys} is not included in the current plan.",
1065
+ // ── settings ──
1066
+ SETTINGS_CHANGE_NOT_FOUND: "No recorded settings change has this id."
686
1067
  };
687
1068
  function formatErrorMessage(template, params = {}) {
688
1069
  return template.replace(/\{(\w+)\}/g, (match, name) => {
@@ -744,6 +1125,8 @@ var ERROR_MESSAGES_DE = {
744
1125
  BUNDLE_VERSION_SUPERSEDED: "BundleVersion '{bundleVersionId}' wurde durch eine neuere Version abgel\xF6st.",
745
1126
  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).",
746
1127
  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.",
1128
+ BUNDLE_VERSION_NO_PRICE: "Diese Bundle-Version kann ohne Preis nicht ver\xF6ffentlicht werden: weder ein Grundpreis noch eine Plan-\xDCberschreibung ergibt einen.",
1129
+ 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.",
747
1130
  BUNDLE_VERSION_DISCARD_NOT_IMPLEMENTED: "Das Verwerfen ist im aktuellen Repository nicht implementiert. Implementiere BundleRepository.deleteDraft.",
748
1131
  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.",
749
1132
  BUNDLE_VERSION_VALID_FROM_INVALID: "validFrom '{validFrom}' ist kein g\xFCltiges Datum",
@@ -757,22 +1140,27 @@ var ERROR_MESSAGES_DE = {
757
1140
  PLAN_VERSION_NOT_FOUND: "PlanVersion '{versionId}' nicht gefunden",
758
1141
  BUNDLE_NOT_FOUND: "Bundle '{bundleId}' nicht gefunden",
759
1142
  BUNDLE_VERSION_NOT_FOUND: "BundleVersion '{bundleVersionId}' nicht gefunden",
760
- FEATURE_NOT_FOUND: "Feature '{featureKey}' im Projekt '{projectKey}' nicht gefunden",
761
- QUOTA_NOT_FOUND: "Quota '{quotaKey}' im Projekt '{projectKey}' nicht gefunden",
1143
+ FEATURE_NOT_FOUND: "Feature '{featureKey}' nicht gefunden",
1144
+ QUOTA_NOT_FOUND: "Quota '{quotaKey}' nicht gefunden",
762
1145
  PROMOTION_NOT_FOUND: "Promotion '{promotionId}' nicht gefunden",
763
1146
  MARKETING_PROJECTION_NOT_FOUND: "MarketingProjection '{projectionId}' nicht gefunden",
764
- PLAN_ALREADY_EXISTS: "Plan '{planKey}' gibt es im Projekt '{projectKey}' bereits",
765
- BUNDLE_ALREADY_EXISTS: "Bundle '{bundleKey}' gibt es im Projekt '{projectKey}' bereits",
1147
+ PLAN_ALREADY_EXISTS: "Plan '{planKey}' gibt es bereits",
1148
+ BUNDLE_ALREADY_EXISTS: "Bundle '{bundleKey}' gibt es bereits",
766
1149
  MARKETING_PROJECTION_ALREADY_EXISTS: "F\xFCr {targetType}/{targetVersionId}/{locale} gibt es bereits eine Marketing-Projektion \u2014 bearbeite sie per PATCH",
767
1150
  PLAN_DRAFT_ALREADY_EXISTS: "Plan '{planKey}' hat bereits die Entwurfsversion v{draftVersion}; ver\xF6ffentliche oder verwirf sie zuerst",
768
1151
  BUNDLE_DRAFT_ALREADY_EXISTS: "Bundle '{bundleKey}' hat bereits die Entwurfsversion v{draftVersion}; ver\xF6ffentliche oder verwirf sie zuerst",
769
1152
  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",
770
1153
  DISCOVERY_STATUS_TRANSITION_INVALID: "Der \xDCbergang '{from}' \u2192 '{to}' ist nicht erlaubt",
771
1154
  DISCOVERY_NOT_INITIALIZED: "Die Freigabe braucht einen Discovery-Snapshot \u2014 Discovery ist nicht initialisiert (#25)",
1155
+ // ── catalogue import ──
1156
+ 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.",
1157
+ PLAN_CATALOG_INVALID: "Der Plankatalog wurde gelesen und dann abgelehnt: {message}",
772
1158
  // ── billing ──
773
1159
  BUNDLE_ALREADY_SUBSCRIBED: "Abonnement '{subscriptionId}' hat dieses Bundle bereits aktiv gebucht.",
774
1160
  BUNDLE_INCOMPATIBLE_WITH_PLAN: "BundleVersion '{bundleVersionId}' passt nicht zum Plan '{planKey}'. Erlaubt: [{allowedPlanKeys}].",
775
1161
  BUNDLE_NOT_SELF_SERVICE: "Bundle '{bundleKey}' wird nur \xFCber einen Sondervertrag freigeschaltet. Bitte wende dich an die Vertragsverwaltung.",
1162
+ 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.",
1163
+ 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.",
776
1164
  SUBSCRIPTION_BUNDLE_ALREADY_CANCELLED: "SubscriptionBundle '{subscriptionBundleId}' ist bereits gek\xFCndigt.",
777
1165
  SUBSCRIPTION_BUNDLE_NOT_CANCELLED: "SubscriptionBundle '{subscriptionBundleId}' ist nicht gek\xFCndigt.",
778
1166
  SUBSCRIPTION_BUNDLE_CANCELLATION_EFFECTIVE: "Die K\xFCndigung ist bereits wirksam \u2014 buche das Bundle einfach neu.",
@@ -781,9 +1169,24 @@ var ERROR_MESSAGES_DE = {
781
1169
  SUBSCRIPTION_BUNDLE_NOT_FOUND: "SubscriptionBundle '{subscriptionBundleId}' nicht gefunden",
782
1170
  TENANT_NOT_FOUND: "Mandant {slug} nicht gefunden",
783
1171
  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.",
784
- PLAN_NOT_IN_CATALOG: 'Plan "{planKey}" steht nicht im Katalog ({projectKey})',
785
- PLAN_NOT_SELF_SERVICE: "{planKey} wird nicht per Self-Service freigeschaltet.",
1172
+ PLAN_NOT_IN_CATALOG: 'Plan "{planKey}" steht nicht im Katalog',
1173
+ PLAN_NOT_SELF_SERVICE: "{planName} wird nur \xFCber einen Sondervertrag freigeschaltet. Bitte wenden Sie sich an die Vertragsverwaltung.",
786
1174
  PLAN_CHANGE_BLOCKED: "W\xE4hrend des Onboardings ist ein Planwechsel gesperrt.",
1175
+ SUBSCRIPTION_CHANGED: "Dieses Abonnement hat sich ge\xE4ndert, w\xE4hrend die Anfrage entschieden wurde. Bitte lade es neu.",
1176
+ NO_SUBSCRIPTION: "Dieser Mandant hat kein Abonnement, das gek\xFCndigt werden kann.",
1177
+ CANCELLATION_TERMS_CHANGED: "Das Wirksamkeitsdatum hat sich seit der Anzeige ge\xE4ndert. Bitte best\xE4tige das neue Datum.",
1178
+ SUBSCRIPTION_ENDED: "Dieses Abonnement ist beendet. Sein Paket kann nicht mehr gewechselt werden.",
1179
+ PLAN_LOCKED: "Aktiver Sondervertrag {planName} \u2014 f\xFCr einen Paketwechsel wenden Sie sich bitte an die Vertragsverwaltung.",
1180
+ QUOTA_OVER_TARGET: "Aktuelle Nutzung {used} \xFCberschreitet das Ziel-Limit {targetMax} ({quotaKey}) im Paket {planName}. Bitte zuerst die Nutzung reduzieren.",
1181
+ 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.",
1182
+ 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.",
1183
+ NO_CHANGE: "Zielpaket und Abrechnungsrhythmus entsprechen bereits dem aktuellen Stand.",
1184
+ 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.",
1185
+ 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.",
1186
+ 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.",
1187
+ 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.",
1188
+ MINIMUM_TERM_BINDS: "Die Mindestlaufzeit reicht \xFCber das Ende der Periode hinaus \u2014 die K\xFCndigung wird erst zum Ende der Mindestlaufzeit wirksam.",
1189
+ BUNDLE_FEATURE_DEPENDENCY_UNSATISFIED: "Das Bundle setzt [{features}] voraus \u2014 weder im Plan noch in den aktiven Bundles vorhanden.",
787
1190
  NO_PENDING_PLAN_VERSION: "Es wartet keine Planversion auf eine Best\xE4tigung.",
788
1191
  ONBOARDING_CREATE_FAILED: "Das Konto konnte nicht angelegt werden. Bitte versuche es noch einmal.",
789
1192
  BUNDLE_PREVIEW_ARGUMENT_AMBIGUOUS: "Genau eines von bundleVersionId (Vorschau f\xFCrs Buchen) oder subscriptionBundleId (Vorschau f\xFCrs K\xFCndigen) muss angegeben sein.",
@@ -815,6 +1218,8 @@ var ERROR_MESSAGES_DE = {
815
1218
  SUBSCRIPTION_CONTRACT_PLAN_LINE_ITEM_REQUIRED: "Ein Abo-Vertrag braucht genau eine Plan-Grundposition.",
816
1219
  SUBSCRIPTION_CONTRACT_INVALID_DATE: "{field} muss ein g\xFCltiges Datum sein.",
817
1220
  SUBSCRIPTION_CONTRACT_INVALID_WINDOW: "effectiveUntil muss nach effectiveFrom liegen.",
1221
+ SUBSCRIPTION_CONTRACT_LINE_ITEM_TAX_MISMATCH: "taxAmount einer Position muss exakt priceGross minus priceNet sein.",
1222
+ SUBSCRIPTION_CONTRACT_LINE_ITEM_CURRENCY_MISMATCH: "Eine Position muss in der W\xE4hrung gebucht sein, in der ihr Vertrag bepreist ist.",
818
1223
  SUBSCRIPTION_CONTRACT_TERMINATION_BEFORE_START: "effectiveUntil muss nach dem effectiveFrom des Vertrags liegen.",
819
1224
  CHECKOUT_OFFER_NOT_FOUND: "CheckoutOffer '{offerId}' nicht gefunden",
820
1225
  CHECKOUT_OFFER_EXPIRED: "Checkout-Angebot '{offerId}' ist abgelaufen \u2014 '{action}' ist nicht mehr m\xF6glich",
@@ -839,16 +1244,23 @@ var ERROR_MESSAGES_DE = {
839
1244
  PLAN_NOT_SELECTED: "Bitte w\xE4hle zuerst einen Plan aus.",
840
1245
  MODEL_NOT_AVAILABLE: "Diese Option steht nicht zur Verf\xFCgung.",
841
1246
  // ── entitlement (code lives in upsell.types.ts) ──
842
- [FEATURE_NOT_LICENSED]: "Das Feature {featureKeys} ist im aktuellen Plan nicht enthalten."
1247
+ [FEATURE_NOT_LICENSED]: "Das Feature {featureKeys} ist im aktuellen Plan nicht enthalten.",
1248
+ // ── settings ──
1249
+ SETTINGS_CHANGE_NOT_FOUND: "Keine aufgezeichnete Einstellungs\xE4nderung hat diese ID."
843
1250
  };
844
1251
  export {
1252
+ ACTIVE_SUBSCRIPTION_CONTRACT_STATUSES,
845
1253
  AUTH_ERROR_CODES,
846
1254
  BILLING_ERROR_CODES,
1255
+ BUNDLE_PRICE_LOOKUP_LIMIT,
1256
+ CATALOGUE_KEYS,
847
1257
  CATALOG_ERROR_CODES,
848
1258
  CONTRACT_ERROR_CODES,
849
1259
  ERROR_MESSAGES_DE,
850
1260
  ERROR_MESSAGES_EN,
851
1261
  FEATURE_NOT_LICENSED,
1262
+ MARKETING_PRIORITY_MAX,
1263
+ MARKETING_PRIORITY_MIN,
852
1264
  OTP_RATE_LIMIT_MAX_SENDS,
853
1265
  OTP_RATE_LIMIT_WINDOW_MINUTES,
854
1266
  OTP_TTL_MINUTES,
@@ -864,25 +1276,41 @@ export {
864
1276
  REGISTRATION_ERROR_CODES,
865
1277
  REGISTRATION_RESUME_TTL_MINUTES,
866
1278
  REGISTRATION_STEP_BY_STATUS,
1279
+ SETTINGS_ERROR_CODES,
867
1280
  SETUP_ERROR_CODES,
868
1281
  applyPromo,
869
1282
  assertPersistenceCapabilities,
870
1283
  buildActivePlanVersionWhere,
871
1284
  buildActiveVersionWhere,
872
1285
  buildFeatureRequiresIndex,
1286
+ bundleDraftDefaults,
1287
+ bundleStemDefaults,
1288
+ canonicalJson,
873
1289
  classifyBundleVersionDiff,
874
1290
  classifyPlanDiff,
875
1291
  collectUnsatisfiedRequires,
876
1292
  coverageExcludingSelf,
1293
+ definedFields,
1294
+ diffSettings,
877
1295
  formatErrorMessage,
878
1296
  isBundleRedundant,
879
1297
  isPlatformUserExistsError,
880
1298
  isVersionEditable,
1299
+ keepOneRecommended,
881
1300
  missingRequiresFor,
882
1301
  pickActivePromo,
1302
+ previousUtcDay,
883
1303
  promoStatus,
1304
+ readQuotaRecord,
1305
+ readQuotaValue,
884
1306
  resolveBundleAvailability,
885
1307
  resolveErrorMessage,
886
1308
  selectChargeableBundles,
887
- startOfUtcDay
1309
+ settingsSubtreeOf,
1310
+ startOfUtcDay,
1311
+ toBundleStemRow,
1312
+ toContractLineItemRecord,
1313
+ toPlanRow,
1314
+ toPlanVersionRow,
1315
+ toSubscriptionContractRecord
888
1316
  };