@saasicat/core 1.0.0-rc.2 → 1.0.0-rc.21

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);
@@ -31,27 +87,33 @@ function pickActivePromo(promotions, targetKey, locale, cycle, today = /* @__PUR
31
87
  return matches[0] ?? null;
32
88
  }
33
89
  function applyPromo(promo, basePrice) {
90
+ const result = boundedPromo(promo, basePrice);
91
+ return result && result.discounted < result.original ? result : null;
92
+ }
93
+ function boundedPromo(promo, basePrice) {
34
94
  if (!promo || basePrice === null || basePrice === void 0) return null;
35
95
  if (promo.type === "percent" && typeof promo.value === "number") {
96
+ const pct = withinBounds(promo.value, 100);
36
97
  return {
37
98
  kind: "percent",
38
- discounted: Math.round(basePrice * (100 - promo.value)) / 100,
99
+ discounted: Math.round(basePrice * (100 - pct)) / 100,
39
100
  original: basePrice,
40
- pct: promo.value
101
+ pct
41
102
  };
42
103
  }
43
104
  if (promo.type === "amount" && typeof promo.value === "number") {
105
+ const saved = withinBounds(promo.value, basePrice);
44
106
  return {
45
107
  kind: "amount",
46
- discounted: Math.max(0, basePrice - promo.value),
108
+ discounted: Math.round((basePrice - saved) * 100) / 100,
47
109
  original: basePrice,
48
- saved: promo.value
110
+ saved
49
111
  };
50
112
  }
51
- if (promo.type === "intro" && typeof promo.value === "object") {
113
+ if (promo.type === "intro" && typeof promo.value === "object" && promo.value !== null) {
52
114
  return {
53
115
  kind: "intro",
54
- discounted: promo.value.price,
116
+ discounted: withinBounds(promo.value.price, basePrice),
55
117
  original: basePrice,
56
118
  months: promo.value.months
57
119
  };
@@ -61,26 +123,38 @@ function applyPromo(promo, basePrice) {
61
123
  }
62
124
  return null;
63
125
  }
126
+ function withinBounds(value, upper) {
127
+ return Math.min(Math.max(value, 0), Math.max(upper, 0));
128
+ }
129
+ function promotionOnPrice(promotions, targetKey, locale, cycle, basePrice, today = /* @__PURE__ */ new Date(), targetType = "PLAN") {
130
+ const promotion = pickActivePromo(promotions, targetKey, locale, cycle, today, targetType);
131
+ const result = applyPromo(promotion, basePrice);
132
+ return promotion && result ? { promotion, result } : null;
133
+ }
134
+
135
+ // src/quota-value.ts
136
+ function readQuotaValue(value) {
137
+ if (typeof value === "number") return Number.isFinite(value) ? value : null;
138
+ if (typeof value !== "string") return null;
139
+ if (value.trim() === "") return null;
140
+ const parsed = Number(value);
141
+ return Number.isFinite(parsed) ? parsed : null;
142
+ }
143
+ var UNCOUNTABLE = -1;
144
+ function readQuotaRecord(value) {
145
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return {};
146
+ const quotas = {};
147
+ for (const [key, entry] of Object.entries(value)) {
148
+ quotas[key] = readQuotaValue(entry) ?? UNCOUNTABLE;
149
+ }
150
+ return quotas;
151
+ }
64
152
 
65
153
  // src/version-diff.ts
66
154
  function classifyPlanDiff(oldV, newV) {
67
155
  const changes = [];
68
156
  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
- );
157
+ appendQuotaChanges(changes, oldV.quotas, newV.quotas);
84
158
  appendDecimalChange(changes, "monthlyNet", oldV.monthlyNet, newV.monthlyNet, "lowerIsBetter");
85
159
  appendDecimalChange(changes, "yearlyNet", oldV.yearlyNet, newV.yearlyNet, "lowerIsBetter");
86
160
  return buildResult(changes);
@@ -127,11 +201,6 @@ function appendFeatureChanges(out, oldFeatures, newFeatures) {
127
201
  });
128
202
  }
129
203
  }
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
204
  function appendDecimalChange(out, field, oldValue, newValue, polarity) {
136
205
  const oldNum = toNumber(oldValue);
137
206
  const newNum = toNumber(newValue);
@@ -162,16 +231,31 @@ function buildResult(changes) {
162
231
  return { nonRegressive, changes };
163
232
  }
164
233
  function appendQuotaChanges(out, oldQuotas, newQuotas) {
234
+ const at = (quotas, key) => Object.hasOwn(quotas, key) ? quotas[key] : 0;
165
235
  const allKeys = /* @__PURE__ */ new Set([...Object.keys(oldQuotas), ...Object.keys(newQuotas)]);
166
236
  for (const key of allKeys) {
167
- const oldValue = oldQuotas[key] ?? 0;
168
- const newValue = newQuotas[key] ?? 0;
237
+ const oldRaw = at(oldQuotas, key);
238
+ const newRaw = at(newQuotas, key);
239
+ if (oldRaw === newRaw) continue;
240
+ const oldValue = toQuotaNumber(oldRaw);
241
+ const newValue = toQuotaNumber(newRaw);
169
242
  if (oldValue === newValue) continue;
170
- const direction = directionFromQuotaCmp(oldValue, newValue);
171
- out.push({ field: `quotas.${key}`, oldValue, newValue, direction });
243
+ out.push({
244
+ field: `quotas.${key}`,
245
+ oldValue: reportableQuota(oldRaw, oldValue),
246
+ newValue: reportableQuota(newRaw, newValue),
247
+ direction: directionFromQuotaCmp(oldValue, newValue)
248
+ });
172
249
  }
173
250
  }
251
+ function toQuotaNumber(value) {
252
+ return readQuotaValue(value) ?? Number.NaN;
253
+ }
254
+ function reportableQuota(raw, asNumber) {
255
+ return Number.isFinite(asNumber) ? asNumber : String(raw);
256
+ }
174
257
  function directionFromQuotaCmp(oldValue, newValue) {
258
+ if (!Number.isFinite(oldValue) || !Number.isFinite(newValue)) return "REGRESSION";
175
259
  if (oldValue === -1 && newValue !== -1) return "REGRESSION";
176
260
  if (newValue === -1 && oldValue !== -1) return "IMPROVEMENT";
177
261
  return newValue > oldValue ? "IMPROVEMENT" : "REGRESSION";
@@ -199,6 +283,60 @@ function appendNullableDecimalChange(out, field, oldValue, newValue, polarity) {
199
283
  appendDecimalChange(out, field, oldValue, newValue, polarity);
200
284
  }
201
285
 
286
+ // src/settings-subtree.ts
287
+ var CATALOGUE_KEYS = /* @__PURE__ */ new Set([
288
+ "schemaVersion",
289
+ "features",
290
+ "plans"
291
+ ]);
292
+ function planCatalogSettingsOf(catalog) {
293
+ return settingsSubtreeOf(catalog);
294
+ }
295
+ function settingsSubtreeOf(catalog) {
296
+ const settings = {};
297
+ for (const [key, value] of Object.entries(catalog)) {
298
+ if (CATALOGUE_KEYS.has(key)) continue;
299
+ if (value === void 0) continue;
300
+ settings[key] = value;
301
+ }
302
+ return settings;
303
+ }
304
+ function canonicalJson(value) {
305
+ const json = JSON.stringify(value, (_key, member) => {
306
+ if (member && typeof member === "object" && !Array.isArray(member)) {
307
+ const sorted = {};
308
+ for (const name of Object.keys(member).sort()) {
309
+ sorted[name] = member[name];
310
+ }
311
+ return sorted;
312
+ }
313
+ return member;
314
+ });
315
+ return json ?? "undefined";
316
+ }
317
+ function isPlainObject(value) {
318
+ return value !== null && typeof value === "object" && !Array.isArray(value);
319
+ }
320
+ function diffSettings(before, after) {
321
+ const differences = [];
322
+ collectDifferences(before, after, [], differences);
323
+ return differences.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
324
+ }
325
+ function isObjectOrAbsent(value) {
326
+ return value === void 0 || isPlainObject(value);
327
+ }
328
+ function collectDifferences(before, after, path, out) {
329
+ if (isObjectOrAbsent(before) && isObjectOrAbsent(after) && (before || after)) {
330
+ const names = /* @__PURE__ */ new Set([...Object.keys(before ?? {}), ...Object.keys(after ?? {})]);
331
+ for (const name of names) {
332
+ collectDifferences(before?.[name], after?.[name], [...path, name], out);
333
+ }
334
+ return;
335
+ }
336
+ if (canonicalJson(before) === canonicalJson(after)) return;
337
+ out.push({ path: path.join("."), before, after });
338
+ }
339
+
202
340
  // src/ports/persistence-ports.types.ts
203
341
  var PersistenceCapabilityError = class extends Error {
204
342
  constructor(missing, requiredBy) {
@@ -269,6 +407,8 @@ var CATALOG_ERROR_CODES = {
269
407
  BUNDLE_VERSION_SUPERSEDED: "BUNDLE_VERSION_SUPERSEDED",
270
408
  BUNDLE_VERSION_REGRESSION: "BUNDLE_VERSION_REGRESSION",
271
409
  BUNDLE_VERSION_ZERO_PRICE: "BUNDLE_VERSION_ZERO_PRICE",
410
+ BUNDLE_VERSION_NO_PRICE: "BUNDLE_VERSION_NO_PRICE",
411
+ BUNDLE_VERSION_NOT_PRICED_FOR_PLAN: "BUNDLE_VERSION_NOT_PRICED_FOR_PLAN",
272
412
  BUNDLE_VERSION_DISCARD_NOT_IMPLEMENTED: "BUNDLE_VERSION_DISCARD_NOT_IMPLEMENTED",
273
413
  BUNDLE_VERSION_VALID_FROM_REQUIRED: "BUNDLE_VERSION_VALID_FROM_REQUIRED",
274
414
  BUNDLE_VERSION_VALID_FROM_INVALID: "BUNDLE_VERSION_VALID_FROM_INVALID",
@@ -287,6 +427,12 @@ var CATALOG_ERROR_CODES = {
287
427
  FEATURE_NOT_FOUND: "FEATURE_NOT_FOUND",
288
428
  QUOTA_NOT_FOUND: "QUOTA_NOT_FOUND",
289
429
  PROMOTION_NOT_FOUND: "PROMOTION_NOT_FOUND",
430
+ /**
431
+ * A promotion's value is not one its type takes: a percentage above 0 and
432
+ * at most 100, an amount above 0, an intro price of at least 0 for a whole
433
+ * number of months, or a whole number of free months.
434
+ */
435
+ PROMOTION_VALUE_INVALID: "PROMOTION_VALUE_INVALID",
290
436
  MARKETING_PROJECTION_NOT_FOUND: "MARKETING_PROJECTION_NOT_FOUND",
291
437
  // ── already exists ──
292
438
  PLAN_ALREADY_EXISTS: "PLAN_ALREADY_EXISTS",
@@ -298,7 +444,12 @@ var CATALOG_ERROR_CODES = {
298
444
  // ── discovery ──
299
445
  QUOTA_NOT_IN_DISCOVERY_SNAPSHOT: "QUOTA_NOT_IN_DISCOVERY_SNAPSHOT",
300
446
  DISCOVERY_STATUS_TRANSITION_INVALID: "DISCOVERY_STATUS_TRANSITION_INVALID",
301
- DISCOVERY_NOT_INITIALIZED: "DISCOVERY_NOT_INITIALIZED"
447
+ DISCOVERY_NOT_INITIALIZED: "DISCOVERY_NOT_INITIALIZED",
448
+ // ── catalogue import ──
449
+ /** The uploaded document is not a plan catalog — unparseable, or not an object. */
450
+ PLAN_CATALOG_UNREADABLE: "PLAN_CATALOG_UNREADABLE",
451
+ /** It parsed, and then failed the schema or a cross-field rule. */
452
+ PLAN_CATALOG_INVALID: "PLAN_CATALOG_INVALID"
302
453
  };
303
454
  var BILLING_ERROR_CODES = {
304
455
  /**
@@ -310,6 +461,8 @@ var BILLING_ERROR_CODES = {
310
461
  BUNDLE_ALREADY_SUBSCRIBED: "BUNDLE_ALREADY_SUBSCRIBED",
311
462
  BUNDLE_INCOMPATIBLE_WITH_PLAN: "BUNDLE_INCOMPATIBLE_WITH_PLAN",
312
463
  BUNDLE_NOT_SELF_SERVICE: "BUNDLE_NOT_SELF_SERVICE",
464
+ BUNDLE_CYCLE_EXCEEDS_PLAN: "BUNDLE_CYCLE_EXCEEDS_PLAN",
465
+ BUNDLE_NOT_PRICED_FOR_THIS_PLAN: "BUNDLE_NOT_PRICED_FOR_THIS_PLAN",
313
466
  SUBSCRIPTION_BUNDLE_ALREADY_CANCELLED: "SUBSCRIPTION_BUNDLE_ALREADY_CANCELLED",
314
467
  SUBSCRIPTION_BUNDLE_NOT_CANCELLED: "SUBSCRIPTION_BUNDLE_NOT_CANCELLED",
315
468
  SUBSCRIPTION_BUNDLE_CANCELLATION_EFFECTIVE: "SUBSCRIPTION_BUNDLE_CANCELLATION_EFFECTIVE",
@@ -324,8 +477,91 @@ var BILLING_ERROR_CODES = {
324
477
  PLAN_NOT_IN_CATALOG: "PLAN_NOT_IN_CATALOG",
325
478
  /** Plan exists but cannot be booked via self-service. */
326
479
  PLAN_NOT_SELF_SERVICE: "PLAN_NOT_SELF_SERVICE",
480
+ /**
481
+ * Plan carries no price for the requested billing cycle, so it is not sold
482
+ * in it: a plan without a yearly price is a monthly plan.
483
+ */
484
+ PLAN_NOT_SOLD_IN_CYCLE: "PLAN_NOT_SOLD_IN_CYCLE",
327
485
  /** Plan change refused. Carries `blockers[]` with their own codes. */
328
486
  PLAN_CHANGE_BLOCKED: "PLAN_CHANGE_BLOCKED",
487
+ /**
488
+ * The subscription moved between the read a request was decided on and the
489
+ * write it attempted, so nothing was written. The caller reloads and asks
490
+ * again.
491
+ */
492
+ SUBSCRIPTION_CHANGED: "SUBSCRIPTION_CHANGED",
493
+ /**
494
+ * The tenant has no subscription to act on.
495
+ *
496
+ * `SUBSCRIPTION_NOT_FOUND` states the same fact on the read routes. Both
497
+ * are already on the wire and a code is renamed only deliberately, so both
498
+ * are named here rather than one being dropped behind a consumer's back.
499
+ */
500
+ NO_SUBSCRIPTION: "NO_SUBSCRIPTION",
501
+ /**
502
+ * The cancellation date the reader was shown is no longer the one the rules
503
+ * return, so the confirmation is refused rather than silently applied.
504
+ * Carries the recomputed dates, so the page can re-ask instead of guessing.
505
+ */
506
+ CANCELLATION_TERMS_CHANGED: "CANCELLATION_TERMS_CHANGED",
507
+ // ── the preview routes' own blockers and warnings ──
508
+ //
509
+ // These travel inside a 200 response, in `blockers[]` and `warnings[]`, not
510
+ // as thrown errors — but they are read by a person and so belong in the same
511
+ // catalogue. Each carries `params` with the values its sentence names. Two
512
+ // routes emit them: the plan-change preview and the bundle preview.
513
+ //
514
+ // Three of them used to construct their code from the subject:
515
+ // `VEHICLES_OVER_TARGET`, `ENTERPRISE_LOCKED`, `PRO_NOT_SELF_SERVICE`. That
516
+ // made the set grow with every quota and plan an installation defines, so no
517
+ // catalogue could be complete against it and no guard could check one. The
518
+ // subject is a parameter now.
519
+ /** The subscription has ended; its plan can no longer be changed. */
520
+ SUBSCRIPTION_ENDED: "SUBSCRIPTION_ENDED",
521
+ /** An active special contract blocks self-service plan changes. */
522
+ PLAN_LOCKED: "PLAN_LOCKED",
523
+ /** Current usage of one quota exceeds what the target plan allows. */
524
+ QUOTA_OVER_TARGET: "QUOTA_OVER_TARGET",
525
+ /** The change drops features the tenant has today. */
526
+ FEATURE_LOST: "FEATURE_LOST",
527
+ FEATURES_LOST: "FEATURES_LOST",
528
+ /** Target plan and cycle already match what is in place. */
529
+ NO_CHANGE: "NO_CHANGE",
530
+ /** A shorter cycle cannot start inside the term already running. */
531
+ CYCLE_SHORTENS_AT_TERM_END: "CYCLE_SHORTENS_AT_TERM_END",
532
+ /** A cancelled subscription cannot change its billing cycle. */
533
+ CANCELLATION_LOCKS_THE_CYCLE: "CANCELLATION_LOCKS_THE_CYCLE",
534
+ /**
535
+ * A bundle the tenant already holds runs past the cycle they are moving to.
536
+ *
537
+ * Its own code rather than `BUNDLE_CYCLE_EXCEEDS_PLAN`, which states the
538
+ * same rule about a booking that has not been made yet. The two need
539
+ * different sentences: this one can name the day the obstacle lifts and
540
+ * tell the reader to cancel the booking, and that advice is wrong for
541
+ * someone who is only about to book. One template cannot serve both.
542
+ */
543
+ BUNDLE_BOOKING_OUTLASTS_TARGET_CYCLE: "BUNDLE_BOOKING_OUTLASTS_TARGET_CYCLE",
544
+ /**
545
+ * Features of the previewed bundle are already covered by the plan or by
546
+ * another booked bundle. A warning rather than a blocker: paying twice is
547
+ * the customer's decision, and the preview only has to say so first.
548
+ */
549
+ REDUNDANT_FEATURES: "REDUNDANT_FEATURES",
550
+ /**
551
+ * A booking's minimum term outlasts the period being cancelled, so the
552
+ * cancellation takes effect at the end of the term, not of the period.
553
+ */
554
+ MINIMUM_TERM_BINDS: "MINIMUM_TERM_BINDS",
555
+ /**
556
+ * The previewed bundle requires features that neither the plan nor an
557
+ * active booking provides.
558
+ *
559
+ * The same string is a `StrictModeWarningCode` in `bundle.types.ts`, where
560
+ * it names the catalogue-authoring reading of the rule and travels with its
561
+ * own message. This declaration is the booking preview's blocker, which a
562
+ * tenant reads and therefore needs a shipped text for.
563
+ */
564
+ BUNDLE_FEATURE_DEPENDENCY_UNSATISFIED: "BUNDLE_FEATURE_DEPENDENCY_UNSATISFIED",
329
565
  NO_PENDING_PLAN_VERSION: "NO_PENDING_PLAN_VERSION",
330
566
  ONBOARDING_CREATE_FAILED: "ONBOARDING_CREATE_FAILED",
331
567
  BUNDLE_PREVIEW_ARGUMENT_AMBIGUOUS: "BUNDLE_PREVIEW_ARGUMENT_AMBIGUOUS",
@@ -345,20 +581,64 @@ var CONTRACT_ERROR_CODES = {
345
581
  CHECKOUT_OFFER_BUNDLE_LINE_ITEMS_REQUIRED: "CHECKOUT_OFFER_BUNDLE_LINE_ITEMS_REQUIRED",
346
582
  CHECKOUT_OFFER_BUNDLE_VERSION_NOT_BOOKABLE: "CHECKOUT_OFFER_BUNDLE_VERSION_NOT_BOOKABLE",
347
583
  CHECKOUT_OFFER_FEATURE_DEPENDENCY_UNSATISFIED: "CHECKOUT_OFFER_FEATURE_DEPENDENCY_UNSATISFIED",
584
+ CHECKOUT_OFFER_PLAN_NOT_OFFERED: "CHECKOUT_OFFER_PLAN_NOT_OFFERED",
585
+ CHECKOUT_OFFER_BUNDLE_NOT_OFFERED: "CHECKOUT_OFFER_BUNDLE_NOT_OFFERED",
586
+ CHECKOUT_OFFER_PROMO_CODE_NOT_ACCEPTED: "CHECKOUT_OFFER_PROMO_CODE_NOT_ACCEPTED",
587
+ CHECKOUT_OFFER_PRICE_NOT_CURRENT: "CHECKOUT_OFFER_PRICE_NOT_CURRENT",
348
588
  SUBSCRIPTION_CONTRACT_LINE_ITEMS_REQUIRED: "SUBSCRIPTION_CONTRACT_LINE_ITEMS_REQUIRED",
349
589
  SUBSCRIPTION_CONTRACT_PLAN_LINE_ITEM_REQUIRED: "SUBSCRIPTION_CONTRACT_PLAN_LINE_ITEM_REQUIRED",
350
590
  SUBSCRIPTION_CONTRACT_INVALID_DATE: "SUBSCRIPTION_CONTRACT_INVALID_DATE",
351
591
  SUBSCRIPTION_CONTRACT_INVALID_WINDOW: "SUBSCRIPTION_CONTRACT_INVALID_WINDOW",
592
+ SUBSCRIPTION_CONTRACT_LINE_ITEM_TAX_MISMATCH: "SUBSCRIPTION_CONTRACT_LINE_ITEM_TAX_MISMATCH",
593
+ SUBSCRIPTION_CONTRACT_TAX_RATE_NOT_PERCENT: "SUBSCRIPTION_CONTRACT_TAX_RATE_NOT_PERCENT",
594
+ SUBSCRIPTION_CONTRACT_LINE_ITEM_CURRENCY_MISMATCH: "SUBSCRIPTION_CONTRACT_LINE_ITEM_CURRENCY_MISMATCH",
595
+ /**
596
+ * The lines do not add up to a total the contract states, counted as often
597
+ * as each falls due in one period. `field` names the total.
598
+ */
599
+ SUBSCRIPTION_CONTRACT_LINES_DO_NOT_ADD_UP: "SUBSCRIPTION_CONTRACT_LINES_DO_NOT_ADD_UP",
600
+ /** Something that takes money off states a negative amount. */
601
+ SUBSCRIPTION_CONTRACT_DISCOUNT_NEGATIVE: "SUBSCRIPTION_CONTRACT_DISCOUNT_NEGATIVE",
352
602
  SUBSCRIPTION_CONTRACT_TERMINATION_BEFORE_START: "SUBSCRIPTION_CONTRACT_TERMINATION_BEFORE_START",
353
603
  // ── stage 3 ──
354
604
  CHECKOUT_OFFER_NOT_FOUND: "CHECKOUT_OFFER_NOT_FOUND",
355
605
  CHECKOUT_OFFER_EXPIRED: "CHECKOUT_OFFER_EXPIRED",
356
606
  CHECKOUT_OFFER_ALREADY_CONSUMED: "CHECKOUT_OFFER_ALREADY_CONSUMED",
357
607
  CHECKOUT_OFFER_NOT_CONSUMED: "CHECKOUT_OFFER_NOT_CONSUMED",
608
+ /**
609
+ * The offer changed between the checks of `conclude` and the transaction
610
+ * that consumed it, so the contract checked is not the one the offer now
611
+ * describes. Nothing was written; load the offer and conclude it again.
612
+ */
613
+ CHECKOUT_OFFER_CHANGED: "CHECKOUT_OFFER_CHANGED",
358
614
  SUBSCRIPTION_CONTRACT_NOT_FOUND: "SUBSCRIPTION_CONTRACT_NOT_FOUND",
359
615
  NO_ACTIVE_SUBSCRIPTION_CONTRACT: "NO_ACTIVE_SUBSCRIPTION_CONTRACT",
360
616
  SUBSCRIPTION_CONTRACT_ALREADY_CLOSED: "SUBSCRIPTION_CONTRACT_ALREADY_CLOSED"
361
617
  };
618
+ var SUBSCRIBER_ERROR_CODES = {
619
+ /**
620
+ * A contract, a plan change or a booking for a tenant that has no
621
+ * subscriber. Nothing is agreed or charged without the party to it.
622
+ * Carries `tenantId`.
623
+ */
624
+ SUBSCRIBER_REQUIRED: "SUBSCRIBER_REQUIRED",
625
+ /** The tenant already has a live subscriber. Carries `tenantId`. */
626
+ SUBSCRIBER_ALREADY_EXISTS: "SUBSCRIBER_ALREADY_EXISTS",
627
+ SUBSCRIBER_NOT_FOUND: "SUBSCRIBER_NOT_FOUND",
628
+ SUBSCRIBER_LEGAL_NAME_REQUIRED: "SUBSCRIBER_LEGAL_NAME_REQUIRED",
629
+ /**
630
+ * A detail that has a form — the country, the invoice email — is not in it,
631
+ * or one sign-up requires — the billing address — is missing. Carries `field`.
632
+ */
633
+ SUBSCRIBER_DETAIL_INVALID: "SUBSCRIBER_DETAIL_INVALID",
634
+ /** A contact change named a field of the legal identity. Carries `field`. */
635
+ SUBSCRIBER_IDENTITY_NOT_A_CONTACT: "SUBSCRIBER_IDENTITY_NOT_A_CONTACT",
636
+ SUBSCRIBER_CORRECTION_REASON_REQUIRED: "SUBSCRIBER_CORRECTION_REASON_REQUIRED",
637
+ SUBSCRIBER_CORRECTION_ACTOR_REQUIRED: "SUBSCRIBER_CORRECTION_ACTOR_REQUIRED",
638
+ SUBSCRIBER_CORRECTION_CHANGES_NOTHING: "SUBSCRIBER_CORRECTION_CHANGES_NOTHING",
639
+ /** The operator declared another legal entity: that is a transfer, not an edit. */
640
+ SUBSCRIBER_TAKEOVER_IS_A_TRANSFER: "SUBSCRIBER_TAKEOVER_IS_A_TRANSFER"
641
+ };
362
642
  var REGISTRATION_ERROR_CODES = {
363
643
  PENDING_REGISTRATION_NOT_FOUND: "PENDING_REGISTRATION_NOT_FOUND",
364
644
  PENDING_REGISTRATION_EXPIRED: "PENDING_REGISTRATION_EXPIRED",
@@ -384,6 +664,12 @@ var AUTH_ERROR_CODES = {
384
664
  /** Neither `tenantId` nor `userId` could be resolved from the request. */
385
665
  TENANT_CONTEXT_MISSING: "TENANT_CONTEXT_MISSING",
386
666
  TENANT_ADMIN_REQUIRED: "TENANT_ADMIN_REQUIRED",
667
+ /**
668
+ * The tenant's billing area — its payment method, and later its invoices
669
+ * and account — needs the billing permission, which the application maps
670
+ * to its roles and the tenant's administrator holds by default.
671
+ */
672
+ BILLING_PERMISSION_REQUIRED: "BILLING_PERMISSION_REQUIRED",
387
673
  SUPER_ADMIN_REQUIRED: "SUPER_ADMIN_REQUIRED",
388
674
  /** TOTP MFA has never been set up for this user. */
389
675
  MFA_NOT_SET_UP: "MFA_NOT_SET_UP",
@@ -411,6 +697,26 @@ var PROMO_ERROR_CODES = {
411
697
  PROMO_WOULD_PRODUCE_ZERO_INVOICE: "PROMO_WOULD_PRODUCE_ZERO_INVOICE",
412
698
  PROMO_MAX_REDEMPTIONS_LOWERED: "PROMO_MAX_REDEMPTIONS_LOWERED"
413
699
  };
700
+ var PAYMENT_ERROR_CODES = {
701
+ /**
702
+ * No gateway account takes new payment methods:
703
+ * `config/saas.yaml#payments.newPaymentMethods` names none.
704
+ */
705
+ PAYMENTS_NOT_CONFIGURED: "PAYMENTS_NOT_CONFIGURED",
706
+ /** A callback arrived for an account `config/saas.yaml#payments.accounts` does not name. Carries `account`. */
707
+ PAYMENT_GATEWAY_ACCOUNT_UNKNOWN: "PAYMENT_GATEWAY_ACCOUNT_UNKNOWN",
708
+ /** A callback the gateway did not send: its signature does not verify. */
709
+ PAYMENT_CALLBACK_REJECTED: "PAYMENT_CALLBACK_REJECTED",
710
+ /**
711
+ * A success or cancel URL at an origin `config/saas.yaml#payments.returnUrlOrigins`
712
+ * does not name. Carries `field`.
713
+ */
714
+ PAYMENT_RETURN_URL_NOT_ALLOWED: "PAYMENT_RETURN_URL_NOT_ALLOWED"
715
+ };
716
+ var SETTINGS_ERROR_CODES = {
717
+ /** No recorded change has this id, or the installation keeps no record at all. */
718
+ SETTINGS_CHANGE_NOT_FOUND: "SETTINGS_CHANGE_NOT_FOUND"
719
+ };
414
720
  var PLATFORM_ERROR_CODES = {
415
721
  ...SETUP_ERROR_CODES,
416
722
  ...AUTH_ERROR_CODES,
@@ -418,7 +724,10 @@ var PLATFORM_ERROR_CODES = {
418
724
  ...CATALOG_ERROR_CODES,
419
725
  ...BILLING_ERROR_CODES,
420
726
  ...CONTRACT_ERROR_CODES,
421
- ...REGISTRATION_ERROR_CODES
727
+ ...SUBSCRIBER_ERROR_CODES,
728
+ ...REGISTRATION_ERROR_CODES,
729
+ ...PAYMENT_ERROR_CODES,
730
+ ...SETTINGS_ERROR_CODES
422
731
  };
423
732
 
424
733
  // src/errors.ts
@@ -497,6 +806,19 @@ function selectChargeableBundles(planFeatures, selectedBundles) {
497
806
  }
498
807
  }
499
808
 
809
+ // src/payment-gateway.types.ts
810
+ var PAYMENT_CALLBACK_REJECTED = "PAYMENT_CALLBACK_REJECTED";
811
+ var PaymentCallbackRejectedError = class extends Error {
812
+ code = PAYMENT_CALLBACK_REJECTED;
813
+ constructor(reason) {
814
+ super(`Payment callback rejected: ${reason}`);
815
+ this.name = "PaymentCallbackRejectedError";
816
+ }
817
+ };
818
+ function isPaymentCallbackRejectedError(err) {
819
+ return err instanceof Error && err.code === PAYMENT_CALLBACK_REJECTED;
820
+ }
821
+
500
822
  // src/registration.types.ts
501
823
  var PENDING_EMAIL_TTL_HOURS = 72;
502
824
  var PENDING_ONBOARDING_TTL_DAYS = 14;
@@ -516,6 +838,442 @@ var REGISTRATION_STEP_BY_STATUS = {
516
838
  };
517
839
  var REGISTRATION_RESUME_TTL_MINUTES = 60;
518
840
 
841
+ // src/plan-mapping.ts
842
+ function toPlanRow(row) {
843
+ return {
844
+ id: row.id,
845
+ planKey: row.planKey,
846
+ label: row.label,
847
+ description: row.description,
848
+ icon: row.icon,
849
+ sortOrder: row.sortOrder,
850
+ createdAt: row.createdAt.toISOString(),
851
+ updatedAt: row.updatedAt.toISOString(),
852
+ deletedAt: toIsoOrNull(row.deletedAt)
853
+ };
854
+ }
855
+ function toPlanVersionRow(row, planKey, fields) {
856
+ const mapped = {
857
+ id: row.id,
858
+ version: row.version,
859
+ baseVersionId: row.baseVersionId,
860
+ planId: planKey,
861
+ features: toFeatureKeys(row.features),
862
+ quotas: toQuotaEffects(row.quotas),
863
+ monthlyNet: String(row.monthlyNet),
864
+ yearlyNet: String(row.yearlyNet),
865
+ marketed: row.marketed,
866
+ publishedAt: toIsoOrNull(row.publishedAt),
867
+ supersededAt: toIsoOrNull(row.supersededAt),
868
+ publishedChanges: Array.isArray(row.publishedChanges) ? row.publishedChanges : null,
869
+ changeNote: row.changeNote,
870
+ nonRegressive: row.nonRegressive,
871
+ validFrom: fields.validityWindows ? toIsoOrNull(row.validFrom) : null,
872
+ validUntil: fields.validityWindows ? toIsoOrNull(row.validUntil) : null,
873
+ createdByUserId: row.createdByUserId,
874
+ publishedByUserId: row.publishedByUserId,
875
+ createdAt: row.createdAt.toISOString(),
876
+ updatedAt: row.updatedAt.toISOString()
877
+ };
878
+ if (fields.endsAt) {
879
+ mapped.endsAt = toIsoOrNull(row.endsAt);
880
+ }
881
+ return mapped;
882
+ }
883
+ function toIsoOrNull(value) {
884
+ return value ? value.toISOString() : null;
885
+ }
886
+ function toFeatureKeys(value) {
887
+ if (!Array.isArray(value)) return [];
888
+ return value.filter((entry) => typeof entry === "string");
889
+ }
890
+ function toQuotaEffects(value) {
891
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
892
+ return {};
893
+ }
894
+ const quotas = {};
895
+ for (const [key, entry] of Object.entries(value)) {
896
+ quotas[key] = readQuotaValue(entry) ?? Number.NaN;
897
+ }
898
+ return quotas;
899
+ }
900
+
901
+ // src/legal-identity.ts
902
+ var LEGAL_IDENTITY_FIELDS = [
903
+ "legalName",
904
+ "vatId",
905
+ "taxNumber"
906
+ ];
907
+ function sameLegalIdentity(before, after) {
908
+ return LEGAL_IDENTITY_FIELDS.every((field) => before[field] === after[field]);
909
+ }
910
+ function movedIdentityFields(before, after) {
911
+ return LEGAL_IDENTITY_FIELDS.filter((field) => before[field] !== after[field]);
912
+ }
913
+
914
+ // src/issuer-identity.ts
915
+ function issuerIdentityOf(issuer) {
916
+ if (!issuer) return null;
917
+ return identityOf({
918
+ legalName: issuer.legalName,
919
+ vatId: issuer.vatId,
920
+ taxNumber: issuer.taxNumber
921
+ });
922
+ }
923
+ function recordedIssuerIdentity(settings) {
924
+ const issuer = settings?.issuer;
925
+ if (issuer === null || typeof issuer !== "object" || Array.isArray(issuer)) return null;
926
+ return identityOf(issuer);
927
+ }
928
+ function identityOf(source) {
929
+ const legalName = textOrNull(source.legalName);
930
+ if (legalName === null) return null;
931
+ return {
932
+ legalName,
933
+ vatId: textOrNull(source.vatId),
934
+ taxNumber: textOrNull(source.taxNumber)
935
+ };
936
+ }
937
+ function classifyIssuerChange(recorded, issuer) {
938
+ const current = issuerIdentityOf(issuer);
939
+ if (!recorded)
940
+ return current ? { kind: "first-naming", identity: current } : { kind: "none-named" };
941
+ if (current && sameLegalIdentity(recorded, current))
942
+ return { kind: "unchanged", identity: current };
943
+ if (!current) {
944
+ const moved2 = LEGAL_IDENTITY_FIELDS.filter((field) => recorded[field] !== null);
945
+ return {
946
+ kind: "undeclared",
947
+ recorded,
948
+ current: null,
949
+ moved: moved2,
950
+ fault: { kind: "names-no-issuer" }
951
+ };
952
+ }
953
+ const moved = movedIdentityFields(recorded, current);
954
+ const fault = faultIn(issuer?.correctionOf, recorded, moved);
955
+ if (fault) return { kind: "undeclared", recorded, current, moved, fault };
956
+ const declaration = issuer?.correctionOf;
957
+ return { kind: "corrected", recorded, current, moved, reason: declaration.reason };
958
+ }
959
+ function faultIn(declaration, recorded, moved) {
960
+ if (!declaration) return { kind: "absent" };
961
+ for (const field of LEGAL_IDENTITY_FIELDS) {
962
+ if (!Object.prototype.hasOwnProperty.call(declaration, field)) continue;
963
+ const declared = textOrNull(declaration[field]);
964
+ if (declared !== recorded[field]) {
965
+ return { kind: "names-another-value", field, declared, recorded: recorded[field] };
966
+ }
967
+ }
968
+ for (const field of moved) {
969
+ if (Object.prototype.hasOwnProperty.call(declaration, field)) continue;
970
+ return { kind: "leaves-a-field-out", field, recorded: recorded[field] };
971
+ }
972
+ return null;
973
+ }
974
+ function textOrNull(value) {
975
+ if (typeof value !== "string") return null;
976
+ const trimmed = value.trim();
977
+ return trimmed === "" ? null : trimmed;
978
+ }
979
+
980
+ // src/subscriber-mapping.ts
981
+ function formatCustomerNumber(prefix, sequence) {
982
+ return `${prefix}${sequence}`;
983
+ }
984
+ function toSubscriberRecord(row, tenantId) {
985
+ return {
986
+ id: row.id,
987
+ customerNumber: formatCustomerNumber(row.customerNumberPrefix, row.customerSequence),
988
+ legalName: row.legalName,
989
+ addressLine1: row.addressLine1,
990
+ addressLine2: row.addressLine2,
991
+ postalCode: row.postalCode,
992
+ city: row.city,
993
+ country: row.country,
994
+ vatId: row.vatId,
995
+ taxNumber: row.taxNumber,
996
+ invoiceEmail: row.invoiceEmail,
997
+ tenantId,
998
+ migrated: row.migrated,
999
+ createdAt: row.createdAt,
1000
+ updatedAt: row.updatedAt
1001
+ };
1002
+ }
1003
+ function toSubscriberCorrectionRecord(row) {
1004
+ return {
1005
+ id: row.id,
1006
+ subscriberId: row.subscriberId,
1007
+ previous: toIdentityValues(row.previous),
1008
+ corrected: toIdentityValues(row.corrected),
1009
+ reason: row.reason,
1010
+ correctedBy: row.correctedBy,
1011
+ correctedAt: row.correctedAt
1012
+ };
1013
+ }
1014
+ function identityCorrectionDelta(current, corrected) {
1015
+ const previous = {};
1016
+ const next = {};
1017
+ for (const field of LEGAL_IDENTITY_FIELDS) {
1018
+ const value = corrected[field];
1019
+ if (value === void 0 || value === current[field]) continue;
1020
+ previous[field] = current[field];
1021
+ next[field] = value;
1022
+ }
1023
+ return {
1024
+ previous,
1025
+ corrected: next
1026
+ };
1027
+ }
1028
+ function contractPartiesOf(subscriber, issuer) {
1029
+ const party = {
1030
+ customerNumber: subscriber.customerNumber,
1031
+ legalName: subscriber.legalName,
1032
+ vatId: subscriber.vatId,
1033
+ taxNumber: subscriber.taxNumber,
1034
+ addressLine1: subscriber.addressLine1,
1035
+ addressLine2: subscriber.addressLine2,
1036
+ postalCode: subscriber.postalCode,
1037
+ city: subscriber.city,
1038
+ country: subscriber.country
1039
+ };
1040
+ return {
1041
+ subscriberId: subscriber.id,
1042
+ subscriber: party,
1043
+ issuer: issuer ? issuerPartyOf(issuer) : null
1044
+ };
1045
+ }
1046
+ function issuerPartyOf(issuer) {
1047
+ const identity = issuerIdentityOf(issuer);
1048
+ return {
1049
+ legalName: identity?.legalName ?? "",
1050
+ vatId: identity?.vatId ?? null,
1051
+ taxNumber: identity?.taxNumber ?? null,
1052
+ // Settled like the three above rather than copied verbatim: one copy
1053
+ // whose halves were settled differently is a row that reads two ways,
1054
+ // and a city written with a trailing space is not another city.
1055
+ addressLine1: settledText(issuer.addressLine1),
1056
+ addressLine2: settledText(issuer.addressLine2),
1057
+ postalCode: settledText(issuer.postalCode),
1058
+ city: settledText(issuer.city),
1059
+ country: settledText(issuer.country)
1060
+ };
1061
+ }
1062
+ function settledText(value) {
1063
+ if (typeof value !== "string") return null;
1064
+ const trimmed = value.trim();
1065
+ return trimmed === "" ? null : trimmed;
1066
+ }
1067
+ function subscriberFromRegistration(pending) {
1068
+ return {
1069
+ legalName: pending.tenantName,
1070
+ invoiceEmail: pending.email,
1071
+ addressLine1: pending.addressLine1,
1072
+ addressLine2: pending.addressLine2,
1073
+ postalCode: pending.postalCode,
1074
+ city: pending.city,
1075
+ country: pending.country,
1076
+ vatId: pending.vatId,
1077
+ taxNumber: pending.taxNumber
1078
+ };
1079
+ }
1080
+ function toIdentityValues(value) {
1081
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return {};
1082
+ const source = value;
1083
+ const values = {};
1084
+ for (const field of LEGAL_IDENTITY_FIELDS) {
1085
+ if (!Object.prototype.hasOwnProperty.call(source, field)) continue;
1086
+ const entry = source[field];
1087
+ values[field] = typeof entry === "string" ? entry : null;
1088
+ }
1089
+ return values;
1090
+ }
1091
+
1092
+ // src/subscriber-payment-method-mapping.ts
1093
+ var PAYMENT_METHOD_TYPES = ["card", "sepa_debit"];
1094
+ var STATUSES = ["ACTIVE", "REPLACED"];
1095
+ function toSubscriberPaymentMethodRecord(row) {
1096
+ return {
1097
+ id: row.id,
1098
+ subscriberId: row.subscriberId,
1099
+ gatewayAccount: row.gatewayAccount,
1100
+ provider: row.provider,
1101
+ customerRef: row.customerRef,
1102
+ paymentMethodRef: row.paymentMethodRef,
1103
+ type: oneOf(PAYMENT_METHOD_TYPES, row.type, "type", row.id),
1104
+ brand: row.brand,
1105
+ last4: row.last4,
1106
+ expiryMonth: row.expiryMonth,
1107
+ expiryYear: row.expiryYear,
1108
+ country: row.country,
1109
+ bankCode: row.bankCode,
1110
+ mandateReference: row.mandateReference,
1111
+ status: oneOf(STATUSES, row.status, "status", row.id),
1112
+ confirmedAt: row.confirmedAt,
1113
+ replacedAt: row.replacedAt,
1114
+ createdAt: row.createdAt
1115
+ };
1116
+ }
1117
+ function subscriberPaymentMethodColumns(data) {
1118
+ return {
1119
+ subscriberId: data.subscriberId,
1120
+ gatewayAccount: data.gatewayAccount,
1121
+ provider: data.provider,
1122
+ customerRef: data.customerRef,
1123
+ paymentMethodRef: data.paymentMethodRef,
1124
+ type: data.type,
1125
+ brand: data.brand,
1126
+ last4: data.last4,
1127
+ expiryMonth: data.expiryMonth,
1128
+ expiryYear: data.expiryYear,
1129
+ country: data.country,
1130
+ bankCode: data.bankCode,
1131
+ mandateReference: data.mandateReference,
1132
+ confirmedAt: data.confirmedAt
1133
+ };
1134
+ }
1135
+ function oneOf(allowed, value, column, id) {
1136
+ const match = allowed.find((entry) => entry === value);
1137
+ if (match === void 0) {
1138
+ throw new Error(
1139
+ `subscriber_payment_methods row '${id}' holds ${column} '${value}', which is none of ${allowed.join(", ")}.`
1140
+ );
1141
+ }
1142
+ return match;
1143
+ }
1144
+ var FOREIGN_PAYMENT_METHOD_REFERENCE = "FOREIGN_PAYMENT_METHOD_REFERENCE";
1145
+ var ForeignPaymentMethodReferenceError = class extends Error {
1146
+ constructor(gatewayAccount, paymentMethodRef) {
1147
+ super(
1148
+ `Payment method '${paymentMethodRef}' of account '${gatewayAccount}' belongs to another subscriber. A reference belongs to exactly one, so it is not handed out.`
1149
+ );
1150
+ this.gatewayAccount = gatewayAccount;
1151
+ this.paymentMethodRef = paymentMethodRef;
1152
+ this.name = "ForeignPaymentMethodReferenceError";
1153
+ }
1154
+ gatewayAccount;
1155
+ paymentMethodRef;
1156
+ code = FOREIGN_PAYMENT_METHOD_REFERENCE;
1157
+ };
1158
+ function isForeignPaymentMethodReferenceError(error) {
1159
+ return error instanceof Error && error.code === FOREIGN_PAYMENT_METHOD_REFERENCE;
1160
+ }
1161
+ function refuseForeignPaymentMethodReference(recorded, subscriberId) {
1162
+ if (recorded.subscriberId === subscriberId) return;
1163
+ throw new ForeignPaymentMethodReferenceError(
1164
+ recorded.gatewayAccount,
1165
+ recorded.paymentMethodRef
1166
+ );
1167
+ }
1168
+
1169
+ // src/subscription-contract-mapping.ts
1170
+ function toSubscriptionContractRecord(row, lineItems) {
1171
+ return {
1172
+ id: row.id,
1173
+ tenantId: row.tenantId,
1174
+ subscriberId: row.subscriberId,
1175
+ subscriber: toSubscriberParty(row.subscriberSnapshot),
1176
+ issuer: isPlainObject2(row.issuerSnapshot) ? toIssuerParty(row.issuerSnapshot) : null,
1177
+ partiesMigrated: row.partiesMigrated,
1178
+ status: row.status,
1179
+ effectiveFrom: row.effectiveFrom,
1180
+ effectiveUntil: row.effectiveUntil,
1181
+ originalOfferId: row.originalOfferId,
1182
+ originalPlanVersionId: row.originalPlanVersionId,
1183
+ originalBundleVersionIds: toStringArray(row.originalBundleVersionIds),
1184
+ entitlementSnapshot: isPlainObject2(row.entitlementSnapshot) ? row.entitlementSnapshot : null,
1185
+ priceSnapshot: row.priceSnapshot,
1186
+ promotionSnapshots: toUnknownArray(row.promotionSnapshots),
1187
+ promoCodeSnapshots: toUnknownArray(row.promoCodeSnapshots),
1188
+ termsSnapshot: toRecordOrNull(row.termsSnapshot),
1189
+ lineItems: lineItems.map(toContractLineItemRecord),
1190
+ createdAt: row.createdAt,
1191
+ updatedAt: row.updatedAt
1192
+ };
1193
+ }
1194
+ function toContractLineItemRecord(row) {
1195
+ return {
1196
+ id: row.id,
1197
+ contractId: row.contractId,
1198
+ kind: row.kind,
1199
+ sourceKey: row.sourceKey,
1200
+ sourceVersionId: row.sourceVersionId,
1201
+ titleSnapshot: row.titleSnapshot,
1202
+ descriptionSnapshot: row.descriptionSnapshot,
1203
+ quantity: row.quantity,
1204
+ unit: row.unit,
1205
+ priceNet: Number(row.priceNet),
1206
+ priceGross: Number(row.priceGross),
1207
+ billingCycle: row.billingCycle,
1208
+ currency: row.currency,
1209
+ taxRate: Number(row.taxRate),
1210
+ taxAmount: Number(row.taxAmount),
1211
+ minimumTermUntil: row.minimumTermUntil,
1212
+ featuresSnapshot: toStringArray(row.featuresSnapshot),
1213
+ quotaEffectsSnapshot: toQuotaEffects2(row.quotaEffectsSnapshot),
1214
+ metadata: toRecordOrNull(row.metadata),
1215
+ createdAt: row.createdAt
1216
+ };
1217
+ }
1218
+ function toSubscriberParty(value) {
1219
+ const source = isPlainObject2(value) ? value : {};
1220
+ return {
1221
+ customerNumber: stringOr(source.customerNumber, ""),
1222
+ ...toIssuerParty(source)
1223
+ };
1224
+ }
1225
+ function toRunningContractIssuer(row) {
1226
+ const issuer = isPlainObject2(row.issuerSnapshot) ? row.issuerSnapshot : null;
1227
+ return {
1228
+ id: row.id,
1229
+ tenantId: row.tenantId,
1230
+ issuerLegalName: issuer ? stringOrNull(issuer.legalName) : null,
1231
+ effectiveFrom: row.effectiveFrom
1232
+ };
1233
+ }
1234
+ function toIssuerParty(value) {
1235
+ const source = value;
1236
+ return {
1237
+ legalName: stringOr(source.legalName, ""),
1238
+ vatId: stringOrNull(source.vatId),
1239
+ taxNumber: stringOrNull(source.taxNumber),
1240
+ addressLine1: stringOrNull(source.addressLine1),
1241
+ addressLine2: stringOrNull(source.addressLine2),
1242
+ postalCode: stringOrNull(source.postalCode),
1243
+ city: stringOrNull(source.city),
1244
+ country: stringOrNull(source.country)
1245
+ };
1246
+ }
1247
+ function stringOr(value, fallback) {
1248
+ return typeof value === "string" ? value : fallback;
1249
+ }
1250
+ function stringOrNull(value) {
1251
+ return typeof value === "string" ? value : null;
1252
+ }
1253
+ function isPlainObject2(value) {
1254
+ return value !== null && typeof value === "object" && !Array.isArray(value);
1255
+ }
1256
+ function toUnknownArray(value) {
1257
+ return Array.isArray(value) ? value : [];
1258
+ }
1259
+ function toRecordOrNull(value) {
1260
+ return isPlainObject2(value) ? value : null;
1261
+ }
1262
+ function toStringArray(value) {
1263
+ if (!Array.isArray(value)) return [];
1264
+ return value.filter((entry) => typeof entry === "string");
1265
+ }
1266
+ function toQuotaEffects2(value) {
1267
+ return isPlainObject2(value) ? readQuotaRecord(value) : {};
1268
+ }
1269
+
1270
+ // src/subscription-contract.types.ts
1271
+ var BUNDLE_PRICE_LOOKUP_LIMIT = 200;
1272
+ var ACTIVE_SUBSCRIPTION_CONTRACT_STATUSES = [
1273
+ "active",
1274
+ "scheduled"
1275
+ ];
1276
+
519
1277
  // src/version-editability.ts
520
1278
  function isVersionEditable(v, now = /* @__PURE__ */ new Date()) {
521
1279
  if (v.publishedAt === null) {
@@ -543,6 +1301,17 @@ function isVersionEditable(v, now = /* @__PURE__ */ new Date()) {
543
1301
  return { editable: true, reason: "pre-active" };
544
1302
  }
545
1303
 
1304
+ // src/recommended-plan.ts
1305
+ function keepOneRecommended(plans, inRequestedLocale) {
1306
+ const recommended = plans.filter((plan) => plan.highlight);
1307
+ if (recommended.length === 0) return null;
1308
+ const winner = recommended.find((plan) => inRequestedLocale.has(plan.planKey)) ?? recommended[0];
1309
+ for (const plan of recommended) {
1310
+ if (plan !== winner) plan.highlight = false;
1311
+ }
1312
+ return winner;
1313
+ }
1314
+
546
1315
  // src/error-messages.ts
547
1316
  var ERROR_MESSAGES_EN = {
548
1317
  // ── setup ──
@@ -556,6 +1325,7 @@ var ERROR_MESSAGES_EN = {
556
1325
  NO_TENANT_ASSIGNED: "No tenant assigned",
557
1326
  TENANT_CONTEXT_MISSING: "No tenant ID found on the request",
558
1327
  TENANT_ADMIN_REQUIRED: "This action requires the TENANT_ADMIN role.",
1328
+ BILLING_PERMISSION_REQUIRED: "This part of the billing area requires the billing permission.",
559
1329
  SUPER_ADMIN_REQUIRED: "Only the SUPER_ADMIN role is allowed",
560
1330
  MFA_NOT_SET_UP: "Run the MFA setup via the CLI first.",
561
1331
  MFA_REQUIRED: "TOTP code required in the X-Mfa-Code header.",
@@ -587,6 +1357,8 @@ var ERROR_MESSAGES_EN = {
587
1357
  BUNDLE_VERSION_SUPERSEDED: "BundleVersion '{bundleVersionId}' has been superseded by a newer version.",
588
1358
  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
1359
  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.",
1360
+ BUNDLE_VERSION_NO_PRICE: "This bundle version cannot be published without a price: neither a base price nor any plan override resolves one.",
1361
+ 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
1362
  BUNDLE_VERSION_DISCARD_NOT_IMPLEMENTED: "Discard is not implemented in the current repository. Implement BundleRepository.deleteDraft.",
591
1363
  BUNDLE_VERSION_VALID_FROM_REQUIRED: "Published bundle versions must keep a validFrom. Set a new future date, or edit the draft before publishing.",
592
1364
  BUNDLE_VERSION_VALID_FROM_INVALID: "validFrom '{validFrom}' is not a valid date",
@@ -600,22 +1372,28 @@ var ERROR_MESSAGES_EN = {
600
1372
  PLAN_VERSION_NOT_FOUND: "PlanVersion '{versionId}' not found",
601
1373
  BUNDLE_NOT_FOUND: "Bundle '{bundleId}' not found",
602
1374
  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}'",
1375
+ FEATURE_NOT_FOUND: "Feature '{featureKey}' not found",
1376
+ QUOTA_NOT_FOUND: "Quota '{quotaKey}' not found",
605
1377
  PROMOTION_NOT_FOUND: "Promotion '{promotionId}' not found",
1378
+ PROMOTION_VALUE_INVALID: "The value of a '{type}' promotion is not one its type takes: a percentage above 0 and at most 100, an amount above 0, an intro price of at least 0 for a whole number of months, or a whole number of free months.",
606
1379
  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}'",
1380
+ PLAN_ALREADY_EXISTS: "Plan '{planKey}' already exists",
1381
+ BUNDLE_ALREADY_EXISTS: "Bundle '{bundleKey}' already exists",
609
1382
  MARKETING_PROJECTION_ALREADY_EXISTS: "Marketing projection for {targetType}/{targetVersionId}/{locale} already exists \u2014 use PATCH to edit it",
610
1383
  PLAN_DRAFT_ALREADY_EXISTS: "Plan '{planKey}' already has a draft version v{draftVersion}; publish or discard it first",
611
1384
  BUNDLE_DRAFT_ALREADY_EXISTS: "Bundle '{bundleKey}' already has a draft version v{draftVersion}; publish or discard it first",
612
1385
  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
1386
  DISCOVERY_STATUS_TRANSITION_INVALID: "Transition '{from}' \u2192 '{to}' is not allowed",
614
1387
  DISCOVERY_NOT_INITIALIZED: "Approval requires a discovery snapshot \u2014 discovery is not initialised (#25)",
1388
+ // ── catalogue import ──
1389
+ 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.",
1390
+ PLAN_CATALOG_INVALID: "The plan catalog was read, and then rejected: {message}",
615
1391
  // ── billing ──
616
1392
  BUNDLE_ALREADY_SUBSCRIBED: "Subscription '{subscriptionId}' has already actively booked this bundle.",
617
1393
  BUNDLE_INCOMPATIBLE_WITH_PLAN: "BundleVersion '{bundleVersionId}' is not compatible with plan '{planKey}'. Allowed: [{allowedPlanKeys}].",
618
1394
  BUNDLE_NOT_SELF_SERVICE: "Bundle '{bundleKey}' is only activated via a special contract. Please contact the contract manager.",
1395
+ 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.",
1396
+ BUNDLE_NOT_PRICED_FOR_THIS_PLAN: "This bundle has no {billingCycle} price for the {planKey} plan, so it cannot be booked from here.",
619
1397
  SUBSCRIPTION_BUNDLE_ALREADY_CANCELLED: "SubscriptionBundle '{subscriptionBundleId}' is already cancelled.",
620
1398
  SUBSCRIPTION_BUNDLE_NOT_CANCELLED: "SubscriptionBundle '{subscriptionBundleId}' is not cancelled.",
621
1399
  SUBSCRIPTION_BUNDLE_CANCELLATION_EFFECTIVE: "Cancellation already in effect \u2014 book the bundle again.",
@@ -624,9 +1402,25 @@ var ERROR_MESSAGES_EN = {
624
1402
  SUBSCRIPTION_BUNDLE_NOT_FOUND: "SubscriptionBundle '{subscriptionBundleId}' not found",
625
1403
  TENANT_NOT_FOUND: "Tenant {slug} not found",
626
1404
  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.",
1405
+ PLAN_NOT_IN_CATALOG: 'Plan "{planKey}" is not in the catalog',
1406
+ PLAN_NOT_SELF_SERVICE: "{planName} is only activated via a special contract. Please contact the contract manager.",
1407
+ PLAN_NOT_SOLD_IN_CYCLE: "{planName} has no price for this billing rhythm and cannot be booked in it.",
629
1408
  PLAN_CHANGE_BLOCKED: "Plan change during onboarding is blocked.",
1409
+ SUBSCRIPTION_CHANGED: "This subscription changed while the request was being decided. Reload it.",
1410
+ NO_SUBSCRIPTION: "This tenant has no subscription to cancel.",
1411
+ CANCELLATION_TERMS_CHANGED: "The effective date changed since it was shown. Confirm the new one.",
1412
+ SUBSCRIPTION_ENDED: "This subscription has ended. Its plan can no longer be changed.",
1413
+ PLAN_LOCKED: "Active {planName} special contract \u2014 please contact the contract manager to change plans.",
1414
+ QUOTA_OVER_TARGET: "Current usage {used} exceeds the target limit {targetMax} ({quotaKey}) in the {planName} plan. Please reduce usage first.",
1415
+ FEATURE_LOST: "Switching means losing access to one feature. Existing data is retained and never deleted \u2014 upgrading again unlocks it.",
1416
+ FEATURES_LOST: "Switching means losing access to {count} features. Existing data is retained and never deleted \u2014 upgrading again unlocks it.",
1417
+ NO_CHANGE: "Target plan and billing cycle already match the current state.",
1418
+ 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.",
1419
+ 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.",
1420
+ 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.",
1421
+ REDUNDANT_FEATURES: "The plan or another booked bundle already includes {count} of the features in this bundle \u2014 booking it pays for them twice.",
1422
+ MINIMUM_TERM_BINDS: "The minimum term extends beyond the end of the period \u2014 the cancellation only takes effect when the minimum term ends.",
1423
+ BUNDLE_FEATURE_DEPENDENCY_UNSATISFIED: "The bundle requires [{features}] \u2014 present neither in the plan nor in the active bundles.",
630
1424
  NO_PENDING_PLAN_VERSION: "There is no pending plan version awaiting confirmation.",
631
1425
  ONBOARDING_CREATE_FAILED: "The account could not be created. Please try again.",
632
1426
  BUNDLE_PREVIEW_ARGUMENT_AMBIGUOUS: "Exactly one of bundleVersionId (add preview) or subscriptionBundleId (cancel preview) must be given.",
@@ -636,7 +1430,7 @@ var ERROR_MESSAGES_EN = {
636
1430
  // ── promo ──
637
1431
  PROMO_CODE_NOT_FOUND: "Code not found",
638
1432
  PROMO_CODE_ALREADY_EXISTS: "The code already exists.",
639
- PROMO_CODE_HAS_REDEMPTIONS: "The code already has redemptions \u2014 it cannot be soft-deleted. Pause it instead.",
1433
+ PROMO_CODE_HAS_REDEMPTIONS: "The code already has redemptions, or checkouts holding one \u2014 it cannot be soft-deleted. Pause it instead.",
640
1434
  PROMO_CODE_NOT_REDEEMABLE: "Code cannot be redeemed: {reason}",
641
1435
  PROMO_CODE_FORMAT_INVALID: 'The code may only contain upper-case letters, digits, "-" and "_" (4\u201332 characters).',
642
1436
  PROMO_PERCENT_OUT_OF_RANGE: "The percentage must be between 0 and 100.",
@@ -654,18 +1448,39 @@ var ERROR_MESSAGES_EN = {
654
1448
  CHECKOUT_OFFER_BUNDLE_LINE_ITEMS_REQUIRED: "Every selected bundle version requires a frozen bundle line item.",
655
1449
  CHECKOUT_OFFER_BUNDLE_VERSION_NOT_BOOKABLE: "At least one bundle version from the checkout offer is no longer bookable.",
656
1450
  CHECKOUT_OFFER_FEATURE_DEPENDENCY_UNSATISFIED: "The selected plan does not cover all feature dependencies: [{missingRequires}] are missing from the plan + selected bundles.",
1451
+ CHECKOUT_OFFER_PLAN_NOT_OFFERED: "Plan '{planKey}' is not offered with a {billingCycle} price at the moment.",
1452
+ CHECKOUT_OFFER_BUNDLE_NOT_OFFERED: "Bundle version '{bundleVersionId}' cannot be added to this offer ({reason}).",
1453
+ CHECKOUT_OFFER_PROMO_CODE_NOT_ACCEPTED: "The promo code cannot be applied to this offer ({reason}).",
1454
+ CHECKOUT_OFFER_PRICE_NOT_CURRENT: "The prices of checkout offer '{offerId}' no longer match the catalogue; create a new offer.",
657
1455
  SUBSCRIPTION_CONTRACT_LINE_ITEMS_REQUIRED: "A subscription contract requires at least one line item.",
658
1456
  SUBSCRIPTION_CONTRACT_PLAN_LINE_ITEM_REQUIRED: "A subscription contract requires exactly one plan base item.",
659
1457
  SUBSCRIPTION_CONTRACT_INVALID_DATE: "{field} must be a valid date.",
660
1458
  SUBSCRIPTION_CONTRACT_INVALID_WINDOW: "effectiveUntil must be after effectiveFrom.",
1459
+ SUBSCRIPTION_CONTRACT_LINE_ITEM_TAX_MISMATCH: "A line item's taxAmount must be exactly priceGross minus priceNet.",
1460
+ SUBSCRIPTION_CONTRACT_TAX_RATE_NOT_PERCENT: "A subscription contract states a tax rate of {taxRate} at {field}, which is not a percentage: a rate lies from 0 to 100, and a value between 0 and 1 is refused as a fraction.",
661
1461
  SUBSCRIPTION_CONTRACT_TERMINATION_BEFORE_START: "effectiveUntil must be after the effectiveFrom of the contract.",
1462
+ SUBSCRIPTION_CONTRACT_LINE_ITEM_CURRENCY_MISMATCH: "A line item must be booked in the currency its contract was priced in.",
1463
+ SUBSCRIPTION_CONTRACT_LINES_DO_NOT_ADD_UP: "The line items of a subscription contract add up to {lines} for {field}, but the contract states {stated}.",
1464
+ SUBSCRIPTION_CONTRACT_DISCOUNT_NEGATIVE: "A subscription contract states a discount of {amount} at {field}; a discount takes money off and is never negative.",
662
1465
  CHECKOUT_OFFER_NOT_FOUND: "CheckoutOffer '{offerId}' not found",
663
1466
  CHECKOUT_OFFER_EXPIRED: "Checkout offer '{offerId}' has expired and cannot be {action}",
664
1467
  CHECKOUT_OFFER_ALREADY_CONSUMED: "Checkout offer '{offerId}' has already been consumed and cannot be {action}",
665
1468
  CHECKOUT_OFFER_NOT_CONSUMED: "CheckoutOffer '{offerId}' must be consumed before the contract is created",
1469
+ CHECKOUT_OFFER_CHANGED: "Checkout offer '{offerId}' changed while it was being concluded. Load it again.",
666
1470
  SUBSCRIPTION_CONTRACT_NOT_FOUND: "SubscriptionContract '{contractId}' not found",
667
1471
  NO_ACTIVE_SUBSCRIPTION_CONTRACT: "No active subscription contract for tenant {tenantId}",
668
1472
  SUBSCRIPTION_CONTRACT_ALREADY_CLOSED: "SubscriptionContract '{contractId}' is already closed",
1473
+ // ── subscriber ──
1474
+ SUBSCRIBER_REQUIRED: "Tenant '{tenantId}' has no subscriber. Nothing is agreed or charged without the party to it: create the tenant's subscriber first.",
1475
+ SUBSCRIBER_ALREADY_EXISTS: "Tenant '{tenantId}' already has a subscriber.",
1476
+ SUBSCRIBER_NOT_FOUND: "Subscriber '{subscriberId}' not found",
1477
+ SUBSCRIBER_LEGAL_NAME_REQUIRED: "A subscriber needs its legal name.",
1478
+ SUBSCRIBER_DETAIL_INVALID: "The subscriber's {field} is not valid.",
1479
+ SUBSCRIBER_IDENTITY_NOT_A_CONTACT: "{field} is part of the subscriber's legal identity and changes only as a correction, with a reason.",
1480
+ SUBSCRIBER_CORRECTION_REASON_REQUIRED: "A correction of the legal identity needs a reason.",
1481
+ SUBSCRIBER_CORRECTION_ACTOR_REQUIRED: "A correction of the legal identity has to say who makes it.",
1482
+ SUBSCRIBER_CORRECTION_CHANGES_NOTHING: "The correction changes nothing: every value it names is already recorded.",
1483
+ SUBSCRIBER_TAKEOVER_IS_A_TRANSFER: "Another legal entity taking over a subscriber is a transfer, not a correction, and cannot be recorded as an edit.",
669
1484
  // ── registration ──
670
1485
  PENDING_REGISTRATION_NOT_FOUND: "This registration could not be found. It may have already been completed or discarded.",
671
1486
  PENDING_REGISTRATION_EXPIRED: "This registration has expired. Please start again.",
@@ -681,8 +1496,15 @@ var ERROR_MESSAGES_EN = {
681
1496
  PLAN_NOT_AVAILABLE: "This plan is not available.",
682
1497
  PLAN_NOT_SELECTED: "Please select a plan first.",
683
1498
  MODEL_NOT_AVAILABLE: "This option is not available.",
1499
+ // ── payments ──
1500
+ PAYMENTS_NOT_CONFIGURED: "Payment methods cannot be set up here yet.",
1501
+ PAYMENT_GATEWAY_ACCOUNT_UNKNOWN: "No payment gateway account is configured under '{account}'.",
1502
+ PAYMENT_CALLBACK_REJECTED: "The payment callback could not be verified.",
1503
+ PAYMENT_RETURN_URL_NOT_ALLOWED: "The {field} leads to a site this installation does not return to.",
684
1504
  // ── entitlement (code lives in upsell.types.ts) ──
685
- [FEATURE_NOT_LICENSED]: "Feature {featureKeys} is not included in the current plan."
1505
+ [FEATURE_NOT_LICENSED]: "Feature {featureKeys} is not included in the current plan.",
1506
+ // ── settings ──
1507
+ SETTINGS_CHANGE_NOT_FOUND: "No recorded settings change has this id."
686
1508
  };
687
1509
  function formatErrorMessage(template, params = {}) {
688
1510
  return template.replace(/\{(\w+)\}/g, (match, name) => {
@@ -713,6 +1535,7 @@ var ERROR_MESSAGES_DE = {
713
1535
  NO_TENANT_ASSIGNED: "Kein Mandant zugeordnet",
714
1536
  TENANT_CONTEXT_MISSING: "Am Request wurde keine Mandanten-ID gefunden",
715
1537
  TENANT_ADMIN_REQUIRED: "Diese Aktion erfordert die Rolle TENANT_ADMIN.",
1538
+ BILLING_PERMISSION_REQUIRED: "Dieser Teil der Abrechnung erfordert die Abrechnungs-Berechtigung.",
716
1539
  SUPER_ADMIN_REQUIRED: "Nur die Rolle SUPER_ADMIN ist zugelassen",
717
1540
  MFA_NOT_SET_UP: "Richte MFA zuerst \xFCber die CLI ein.",
718
1541
  MFA_REQUIRED: "TOTP-Code im Header X-Mfa-Code erforderlich.",
@@ -744,6 +1567,8 @@ var ERROR_MESSAGES_DE = {
744
1567
  BUNDLE_VERSION_SUPERSEDED: "BundleVersion '{bundleVersionId}' wurde durch eine neuere Version abgel\xF6st.",
745
1568
  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
1569
  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.",
1570
+ BUNDLE_VERSION_NO_PRICE: "Diese Bundle-Version kann ohne Preis nicht ver\xF6ffentlicht werden: weder ein Grundpreis noch eine Plan-\xDCberschreibung ergibt einen.",
1571
+ 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
1572
  BUNDLE_VERSION_DISCARD_NOT_IMPLEMENTED: "Das Verwerfen ist im aktuellen Repository nicht implementiert. Implementiere BundleRepository.deleteDraft.",
748
1573
  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
1574
  BUNDLE_VERSION_VALID_FROM_INVALID: "validFrom '{validFrom}' ist kein g\xFCltiges Datum",
@@ -757,22 +1582,28 @@ var ERROR_MESSAGES_DE = {
757
1582
  PLAN_VERSION_NOT_FOUND: "PlanVersion '{versionId}' nicht gefunden",
758
1583
  BUNDLE_NOT_FOUND: "Bundle '{bundleId}' nicht gefunden",
759
1584
  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",
1585
+ FEATURE_NOT_FOUND: "Feature '{featureKey}' nicht gefunden",
1586
+ QUOTA_NOT_FOUND: "Quota '{quotaKey}' nicht gefunden",
762
1587
  PROMOTION_NOT_FOUND: "Promotion '{promotionId}' nicht gefunden",
1588
+ PROMOTION_VALUE_INVALID: "Der Wert einer Aktion vom Typ '{type}' passt nicht zu ihrem Typ: ein Prozentsatz \xFCber 0 und h\xF6chstens 100, ein Betrag \xFCber 0, ein Einf\xFChrungspreis ab 0 f\xFCr eine ganze Zahl von Monaten oder eine ganze Zahl von Gratismonaten.",
763
1589
  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",
1590
+ PLAN_ALREADY_EXISTS: "Plan '{planKey}' gibt es bereits",
1591
+ BUNDLE_ALREADY_EXISTS: "Bundle '{bundleKey}' gibt es bereits",
766
1592
  MARKETING_PROJECTION_ALREADY_EXISTS: "F\xFCr {targetType}/{targetVersionId}/{locale} gibt es bereits eine Marketing-Projektion \u2014 bearbeite sie per PATCH",
767
1593
  PLAN_DRAFT_ALREADY_EXISTS: "Plan '{planKey}' hat bereits die Entwurfsversion v{draftVersion}; ver\xF6ffentliche oder verwirf sie zuerst",
768
1594
  BUNDLE_DRAFT_ALREADY_EXISTS: "Bundle '{bundleKey}' hat bereits die Entwurfsversion v{draftVersion}; ver\xF6ffentliche oder verwirf sie zuerst",
769
1595
  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
1596
  DISCOVERY_STATUS_TRANSITION_INVALID: "Der \xDCbergang '{from}' \u2192 '{to}' ist nicht erlaubt",
771
1597
  DISCOVERY_NOT_INITIALIZED: "Die Freigabe braucht einen Discovery-Snapshot \u2014 Discovery ist nicht initialisiert (#25)",
1598
+ // ── catalogue import ──
1599
+ 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.",
1600
+ PLAN_CATALOG_INVALID: "Der Plankatalog wurde gelesen und dann abgelehnt: {message}",
772
1601
  // ── billing ──
773
1602
  BUNDLE_ALREADY_SUBSCRIBED: "Abonnement '{subscriptionId}' hat dieses Bundle bereits aktiv gebucht.",
774
1603
  BUNDLE_INCOMPATIBLE_WITH_PLAN: "BundleVersion '{bundleVersionId}' passt nicht zum Plan '{planKey}'. Erlaubt: [{allowedPlanKeys}].",
775
1604
  BUNDLE_NOT_SELF_SERVICE: "Bundle '{bundleKey}' wird nur \xFCber einen Sondervertrag freigeschaltet. Bitte wende dich an die Vertragsverwaltung.",
1605
+ 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.",
1606
+ 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
1607
  SUBSCRIPTION_BUNDLE_ALREADY_CANCELLED: "SubscriptionBundle '{subscriptionBundleId}' ist bereits gek\xFCndigt.",
777
1608
  SUBSCRIPTION_BUNDLE_NOT_CANCELLED: "SubscriptionBundle '{subscriptionBundleId}' ist nicht gek\xFCndigt.",
778
1609
  SUBSCRIPTION_BUNDLE_CANCELLATION_EFFECTIVE: "Die K\xFCndigung ist bereits wirksam \u2014 buche das Bundle einfach neu.",
@@ -781,9 +1612,25 @@ var ERROR_MESSAGES_DE = {
781
1612
  SUBSCRIPTION_BUNDLE_NOT_FOUND: "SubscriptionBundle '{subscriptionBundleId}' nicht gefunden",
782
1613
  TENANT_NOT_FOUND: "Mandant {slug} nicht gefunden",
783
1614
  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.",
1615
+ PLAN_NOT_IN_CATALOG: 'Plan "{planKey}" steht nicht im Katalog',
1616
+ PLAN_NOT_SELF_SERVICE: "{planName} wird nur \xFCber einen Sondervertrag freigeschaltet. Bitte wenden Sie sich an die Vertragsverwaltung.",
1617
+ PLAN_NOT_SOLD_IN_CYCLE: "{planName} hat f\xFCr diesen Abrechnungsrhythmus keinen Preis und kann darin nicht gebucht werden.",
786
1618
  PLAN_CHANGE_BLOCKED: "W\xE4hrend des Onboardings ist ein Planwechsel gesperrt.",
1619
+ SUBSCRIPTION_CHANGED: "Dieses Abonnement hat sich ge\xE4ndert, w\xE4hrend die Anfrage entschieden wurde. Bitte lade es neu.",
1620
+ NO_SUBSCRIPTION: "Dieser Mandant hat kein Abonnement, das gek\xFCndigt werden kann.",
1621
+ CANCELLATION_TERMS_CHANGED: "Das Wirksamkeitsdatum hat sich seit der Anzeige ge\xE4ndert. Bitte best\xE4tige das neue Datum.",
1622
+ SUBSCRIPTION_ENDED: "Dieses Abonnement ist beendet. Sein Paket kann nicht mehr gewechselt werden.",
1623
+ PLAN_LOCKED: "Aktiver Sondervertrag {planName} \u2014 f\xFCr einen Paketwechsel wenden Sie sich bitte an die Vertragsverwaltung.",
1624
+ QUOTA_OVER_TARGET: "Aktuelle Nutzung {used} \xFCberschreitet das Ziel-Limit {targetMax} ({quotaKey}) im Paket {planName}. Bitte zuerst die Nutzung reduzieren.",
1625
+ 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.",
1626
+ 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.",
1627
+ NO_CHANGE: "Zielpaket und Abrechnungsrhythmus entsprechen bereits dem aktuellen Stand.",
1628
+ 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.",
1629
+ 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.",
1630
+ 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.",
1631
+ 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.",
1632
+ MINIMUM_TERM_BINDS: "Die Mindestlaufzeit reicht \xFCber das Ende der Periode hinaus \u2014 die K\xFCndigung wird erst zum Ende der Mindestlaufzeit wirksam.",
1633
+ BUNDLE_FEATURE_DEPENDENCY_UNSATISFIED: "Das Bundle setzt [{features}] voraus \u2014 weder im Plan noch in den aktiven Bundles vorhanden.",
787
1634
  NO_PENDING_PLAN_VERSION: "Es wartet keine Planversion auf eine Best\xE4tigung.",
788
1635
  ONBOARDING_CREATE_FAILED: "Das Konto konnte nicht angelegt werden. Bitte versuche es noch einmal.",
789
1636
  BUNDLE_PREVIEW_ARGUMENT_AMBIGUOUS: "Genau eines von bundleVersionId (Vorschau f\xFCrs Buchen) oder subscriptionBundleId (Vorschau f\xFCrs K\xFCndigen) muss angegeben sein.",
@@ -793,7 +1640,7 @@ var ERROR_MESSAGES_DE = {
793
1640
  // ── promo ──
794
1641
  PROMO_CODE_NOT_FOUND: "Code nicht gefunden",
795
1642
  PROMO_CODE_ALREADY_EXISTS: "Diesen Code gibt es bereits.",
796
- PROMO_CODE_HAS_REDEMPTIONS: "Der Code wurde bereits eingel\xF6st \u2014 er l\xE4sst sich nicht per Soft-Delete entfernen. Pausiere ihn stattdessen.",
1643
+ PROMO_CODE_HAS_REDEMPTIONS: "Der Code wurde bereits eingel\xF6st oder ist f\xFCr einen laufenden Checkout reserviert \u2014 er l\xE4sst sich nicht per Soft-Delete entfernen. Pausiere ihn stattdessen.",
797
1644
  PROMO_CODE_NOT_REDEEMABLE: "Der Code l\xE4sst sich nicht einl\xF6sen: {reason}",
798
1645
  PROMO_CODE_FORMAT_INVALID: 'Der Code darf nur Gro\xDFbuchstaben, Ziffern, "-" und "_" enthalten (4\u201332 Zeichen).',
799
1646
  PROMO_PERCENT_OUT_OF_RANGE: "Der Prozentwert muss zwischen 0 und 100 liegen.",
@@ -811,18 +1658,39 @@ var ERROR_MESSAGES_DE = {
811
1658
  CHECKOUT_OFFER_BUNDLE_LINE_ITEMS_REQUIRED: "Jede gew\xE4hlte Bundle-Version braucht eine eingefrorene Bundle-Position.",
812
1659
  CHECKOUT_OFFER_BUNDLE_VERSION_NOT_BOOKABLE: "Mindestens eine Bundle-Version aus dem Checkout-Angebot ist nicht mehr buchbar.",
813
1660
  CHECKOUT_OFFER_FEATURE_DEPENDENCY_UNSATISFIED: "Der gew\xE4hlte Plan deckt nicht alle Feature-Abh\xE4ngigkeiten ab: [{missingRequires}] fehlen in Plan und gew\xE4hlten Bundles.",
1661
+ CHECKOUT_OFFER_PLAN_NOT_OFFERED: "Plan '{planKey}' wird derzeit nicht mit einem Preis f\xFCr den Rhythmus {billingCycle} angeboten.",
1662
+ CHECKOUT_OFFER_BUNDLE_NOT_OFFERED: "Bundle-Version '{bundleVersionId}' kann diesem Angebot nicht hinzugef\xFCgt werden ({reason}).",
1663
+ CHECKOUT_OFFER_PROMO_CODE_NOT_ACCEPTED: "Der Rabattcode kann auf dieses Angebot nicht angewendet werden ({reason}).",
1664
+ CHECKOUT_OFFER_PRICE_NOT_CURRENT: "Die Preise des Angebots '{offerId}' stimmen nicht mehr mit dem Katalog \xFCberein; bitte ein neues Angebot erstellen.",
814
1665
  SUBSCRIPTION_CONTRACT_LINE_ITEMS_REQUIRED: "Ein Abo-Vertrag braucht mindestens eine Position.",
815
1666
  SUBSCRIPTION_CONTRACT_PLAN_LINE_ITEM_REQUIRED: "Ein Abo-Vertrag braucht genau eine Plan-Grundposition.",
816
1667
  SUBSCRIPTION_CONTRACT_INVALID_DATE: "{field} muss ein g\xFCltiges Datum sein.",
817
1668
  SUBSCRIPTION_CONTRACT_INVALID_WINDOW: "effectiveUntil muss nach effectiveFrom liegen.",
1669
+ SUBSCRIPTION_CONTRACT_LINE_ITEM_TAX_MISMATCH: "taxAmount einer Position muss exakt priceGross minus priceNet sein.",
1670
+ SUBSCRIPTION_CONTRACT_TAX_RATE_NOT_PERCENT: "Ein Abo-Vertrag nennt bei {field} einen Steuersatz von {taxRate}, der kein Prozentsatz ist: Ein Satz liegt zwischen 0 und 100, und ein Wert zwischen 0 und 1 wird als Bruch abgelehnt.",
1671
+ SUBSCRIPTION_CONTRACT_LINE_ITEM_CURRENCY_MISMATCH: "Eine Position muss in der W\xE4hrung gebucht sein, in der ihr Vertrag bepreist ist.",
1672
+ SUBSCRIPTION_CONTRACT_LINES_DO_NOT_ADD_UP: "Die Positionen eines Abo-Vertrags ergeben {lines} f\xFCr {field}, der Vertrag nennt aber {stated}.",
1673
+ SUBSCRIPTION_CONTRACT_DISCOUNT_NEGATIVE: "Ein Abo-Vertrag nennt bei {field} einen Rabatt von {amount}; ein Rabatt zieht Geld ab und ist nie negativ.",
818
1674
  SUBSCRIPTION_CONTRACT_TERMINATION_BEFORE_START: "effectiveUntil muss nach dem effectiveFrom des Vertrags liegen.",
819
1675
  CHECKOUT_OFFER_NOT_FOUND: "CheckoutOffer '{offerId}' nicht gefunden",
820
1676
  CHECKOUT_OFFER_EXPIRED: "Checkout-Angebot '{offerId}' ist abgelaufen \u2014 '{action}' ist nicht mehr m\xF6glich",
821
1677
  CHECKOUT_OFFER_ALREADY_CONSUMED: "Checkout-Angebot '{offerId}' wurde bereits eingel\xF6st \u2014 '{action}' ist nicht mehr m\xF6glich",
822
1678
  CHECKOUT_OFFER_NOT_CONSUMED: "CheckoutOffer '{offerId}' muss eingel\xF6st sein, bevor der Vertrag entsteht",
1679
+ CHECKOUT_OFFER_CHANGED: "Checkout-Angebot '{offerId}' hat sich w\xE4hrend des Abschlusses ge\xE4ndert. Bitte neu laden.",
823
1680
  SUBSCRIPTION_CONTRACT_NOT_FOUND: "SubscriptionContract '{contractId}' nicht gefunden",
824
1681
  NO_ACTIVE_SUBSCRIPTION_CONTRACT: "Kein aktiver Abo-Vertrag f\xFCr Mandant {tenantId}",
825
1682
  SUBSCRIPTION_CONTRACT_ALREADY_CLOSED: "SubscriptionContract '{contractId}' ist bereits geschlossen",
1683
+ // ── subscriber ──
1684
+ SUBSCRIBER_REQUIRED: "Mandant '{tenantId}' hat keinen Vertragspartner. Ohne ihn wird nichts vereinbart oder berechnet: Zuerst muss der Vertragspartner des Mandanten angelegt werden.",
1685
+ SUBSCRIBER_ALREADY_EXISTS: "Mandant '{tenantId}' hat bereits einen Vertragspartner.",
1686
+ SUBSCRIBER_NOT_FOUND: "Vertragspartner '{subscriberId}' nicht gefunden",
1687
+ SUBSCRIBER_LEGAL_NAME_REQUIRED: "Ein Vertragspartner braucht seinen rechtlichen Namen.",
1688
+ SUBSCRIBER_DETAIL_INVALID: "Das Feld {field} des Vertragspartners ist ung\xFCltig.",
1689
+ SUBSCRIBER_IDENTITY_NOT_A_CONTACT: "{field} geh\xF6rt zur rechtlichen Identit\xE4t des Vertragspartners und \xE4ndert sich nur als Korrektur mit Begr\xFCndung.",
1690
+ SUBSCRIBER_CORRECTION_REASON_REQUIRED: "Eine Korrektur der rechtlichen Identit\xE4t braucht eine Begr\xFCndung.",
1691
+ SUBSCRIBER_CORRECTION_ACTOR_REQUIRED: "Eine Korrektur der rechtlichen Identit\xE4t muss nennen, wer sie vornimmt.",
1692
+ SUBSCRIBER_CORRECTION_CHANGES_NOTHING: "Die Korrektur \xE4ndert nichts: Jeder genannte Wert ist bereits erfasst.",
1693
+ SUBSCRIBER_TAKEOVER_IS_A_TRANSFER: "\xDCbernimmt eine andere Rechtsperson, ist das eine \xDCbertragung und keine Korrektur; sie l\xE4sst sich nicht als \xC4nderung erfassen.",
826
1694
  // ── registration ──
827
1695
  PENDING_REGISTRATION_NOT_FOUND: "Diese Registrierung konnten wir nicht finden. Vielleicht ist sie bereits abgeschlossen oder verworfen.",
828
1696
  PENDING_REGISTRATION_EXPIRED: "Diese Registrierung ist abgelaufen. Bitte fange noch einmal von vorn an.",
@@ -838,51 +1706,104 @@ var ERROR_MESSAGES_DE = {
838
1706
  PLAN_NOT_AVAILABLE: "Dieser Plan steht nicht zur Verf\xFCgung.",
839
1707
  PLAN_NOT_SELECTED: "Bitte w\xE4hle zuerst einen Plan aus.",
840
1708
  MODEL_NOT_AVAILABLE: "Diese Option steht nicht zur Verf\xFCgung.",
1709
+ // ── payments ──
1710
+ PAYMENTS_NOT_CONFIGURED: "Zahlungsmethoden k\xF6nnen hier noch nicht hinterlegt werden.",
1711
+ PAYMENT_GATEWAY_ACCOUNT_UNKNOWN: "Unter '{account}' ist kein Konto eines Zahlungsanbieters eingerichtet.",
1712
+ PAYMENT_CALLBACK_REJECTED: "Die R\xFCckmeldung des Zahlungsanbieters lie\xDF sich nicht pr\xFCfen.",
1713
+ PAYMENT_RETURN_URL_NOT_ALLOWED: "Die {field} f\xFChrt auf eine Seite, zu der diese Installation nicht zur\xFCckleitet.",
841
1714
  // ── entitlement (code lives in upsell.types.ts) ──
842
- [FEATURE_NOT_LICENSED]: "Das Feature {featureKeys} ist im aktuellen Plan nicht enthalten."
1715
+ [FEATURE_NOT_LICENSED]: "Das Feature {featureKeys} ist im aktuellen Plan nicht enthalten.",
1716
+ // ── settings ──
1717
+ SETTINGS_CHANGE_NOT_FOUND: "Keine aufgezeichnete Einstellungs\xE4nderung hat diese ID."
843
1718
  };
844
1719
  export {
1720
+ ACTIVE_SUBSCRIPTION_CONTRACT_STATUSES,
845
1721
  AUTH_ERROR_CODES,
846
1722
  BILLING_ERROR_CODES,
1723
+ BUNDLE_PRICE_LOOKUP_LIMIT,
1724
+ CATALOGUE_KEYS,
847
1725
  CATALOG_ERROR_CODES,
848
1726
  CONTRACT_ERROR_CODES,
849
1727
  ERROR_MESSAGES_DE,
850
1728
  ERROR_MESSAGES_EN,
851
1729
  FEATURE_NOT_LICENSED,
1730
+ ForeignPaymentMethodReferenceError,
1731
+ LEGAL_IDENTITY_FIELDS,
1732
+ MARKETING_PRIORITY_MAX,
1733
+ MARKETING_PRIORITY_MIN,
852
1734
  OTP_RATE_LIMIT_MAX_SENDS,
853
1735
  OTP_RATE_LIMIT_WINDOW_MINUTES,
854
1736
  OTP_TTL_MINUTES,
855
1737
  OTP_VERIFY_MAX_ATTEMPTS,
856
1738
  PASSWORD_RESET_TTL_MINUTES,
1739
+ PAYMENT_ERROR_CODES,
1740
+ PAYMENT_METHOD_TYPES,
857
1741
  PENDING_CHECKOUT_TTL_DAYS,
858
1742
  PENDING_EMAIL_TTL_HOURS,
859
1743
  PENDING_ONBOARDING_TTL_DAYS,
860
1744
  PLATFORM_ERROR_CODES,
861
1745
  PROMO_ERROR_CODES,
1746
+ PaymentCallbackRejectedError,
862
1747
  PersistenceCapabilityError,
863
1748
  PlatformUserExistsError,
864
1749
  REGISTRATION_ERROR_CODES,
865
1750
  REGISTRATION_RESUME_TTL_MINUTES,
866
1751
  REGISTRATION_STEP_BY_STATUS,
1752
+ SETTINGS_ERROR_CODES,
867
1753
  SETUP_ERROR_CODES,
1754
+ SUBSCRIBER_ERROR_CODES,
868
1755
  applyPromo,
869
1756
  assertPersistenceCapabilities,
870
1757
  buildActivePlanVersionWhere,
871
1758
  buildActiveVersionWhere,
872
1759
  buildFeatureRequiresIndex,
1760
+ bundleDraftDefaults,
1761
+ bundleStemDefaults,
1762
+ canonicalJson,
873
1763
  classifyBundleVersionDiff,
1764
+ classifyIssuerChange,
874
1765
  classifyPlanDiff,
875
1766
  collectUnsatisfiedRequires,
1767
+ contractPartiesOf,
876
1768
  coverageExcludingSelf,
1769
+ definedFields,
1770
+ diffSettings,
1771
+ formatCustomerNumber,
877
1772
  formatErrorMessage,
1773
+ identityCorrectionDelta,
878
1774
  isBundleRedundant,
1775
+ isForeignPaymentMethodReferenceError,
1776
+ isPaymentCallbackRejectedError,
879
1777
  isPlatformUserExistsError,
880
1778
  isVersionEditable,
1779
+ issuerIdentityOf,
1780
+ keepOneRecommended,
881
1781
  missingRequiresFor,
1782
+ movedIdentityFields,
882
1783
  pickActivePromo,
1784
+ planCatalogSettingsOf,
1785
+ previousUtcDay,
883
1786
  promoStatus,
1787
+ promotionOnPrice,
1788
+ readQuotaRecord,
1789
+ readQuotaValue,
1790
+ recordedIssuerIdentity,
1791
+ refuseForeignPaymentMethodReference,
884
1792
  resolveBundleAvailability,
885
1793
  resolveErrorMessage,
1794
+ sameLegalIdentity,
886
1795
  selectChargeableBundles,
887
- startOfUtcDay
1796
+ settingsSubtreeOf,
1797
+ startOfUtcDay,
1798
+ subscriberFromRegistration,
1799
+ subscriberPaymentMethodColumns,
1800
+ toBundleStemRow,
1801
+ toContractLineItemRecord,
1802
+ toPlanRow,
1803
+ toPlanVersionRow,
1804
+ toRunningContractIssuer,
1805
+ toSubscriberCorrectionRecord,
1806
+ toSubscriberPaymentMethodRecord,
1807
+ toSubscriberRecord,
1808
+ toSubscriptionContractRecord
888
1809
  };