@saasicat/core 1.0.0-rc.22 → 1.0.0-rc.24

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.cjs CHANGED
@@ -27,13 +27,20 @@ __export(index_exports, {
27
27
  CATALOGUE_KEYS: () => CATALOGUE_KEYS,
28
28
  CATALOG_ERROR_CODES: () => CATALOG_ERROR_CODES,
29
29
  CONTRACT_ERROR_CODES: () => CONTRACT_ERROR_CODES,
30
+ CUSTOM_LIMITS_KEYS: () => CUSTOM_LIMITS_KEYS,
30
31
  ERROR_MESSAGES_DE: () => ERROR_MESSAGES_DE,
31
32
  ERROR_MESSAGES_EN: () => ERROR_MESSAGES_EN,
32
33
  FEATURE_NOT_LICENSED: () => FEATURE_NOT_LICENSED,
33
34
  ForeignPaymentMethodReferenceError: () => ForeignPaymentMethodReferenceError,
34
35
  LEGAL_IDENTITY_FIELDS: () => LEGAL_IDENTITY_FIELDS,
36
+ MAINTENANCE_CAPABILITY: () => MAINTENANCE_CAPABILITY,
37
+ MAINTENANCE_ERROR_CODES: () => MAINTENANCE_ERROR_CODES,
38
+ MAINTENANCE_MESSAGE_MAX_LENGTH: () => MAINTENANCE_MESSAGE_MAX_LENGTH,
39
+ MAINTENANCE_STATE_MAX_AGE_SECONDS: () => MAINTENANCE_STATE_MAX_AGE_SECONDS,
35
40
  MARKETING_PRIORITY_MAX: () => MARKETING_PRIORITY_MAX,
36
41
  MARKETING_PRIORITY_MIN: () => MARKETING_PRIORITY_MIN,
42
+ NO_SCHEDULED_CHANGE: () => NO_SCHEDULED_CHANGE,
43
+ OPTIONAL_CANONICAL_MODELS: () => OPTIONAL_CANONICAL_MODELS,
37
44
  OTP_RATE_LIMIT_MAX_SENDS: () => OTP_RATE_LIMIT_MAX_SENDS,
38
45
  OTP_RATE_LIMIT_WINDOW_MINUTES: () => OTP_RATE_LIMIT_WINDOW_MINUTES,
39
46
  OTP_TTL_MINUTES: () => OTP_TTL_MINUTES,
@@ -48,62 +55,108 @@ __export(index_exports, {
48
55
  PROMO_ERROR_CODES: () => PROMO_ERROR_CODES,
49
56
  PaymentCallbackRejectedError: () => PaymentCallbackRejectedError,
50
57
  PersistenceCapabilityError: () => PersistenceCapabilityError,
58
+ PersistenceRefusal: () => PersistenceRefusal,
51
59
  PlatformUserExistsError: () => PlatformUserExistsError,
52
60
  REGISTRATION_ERROR_CODES: () => REGISTRATION_ERROR_CODES,
53
61
  REGISTRATION_RESUME_TTL_MINUTES: () => REGISTRATION_RESUME_TTL_MINUTES,
54
62
  REGISTRATION_STEP_BY_STATUS: () => REGISTRATION_STEP_BY_STATUS,
63
+ RETIREMENT_REMINDER_LEAD_DAYS: () => RETIREMENT_REMINDER_LEAD_DAYS,
55
64
  SETTINGS_ERROR_CODES: () => SETTINGS_ERROR_CODES,
56
65
  SETUP_ERROR_CODES: () => SETUP_ERROR_CODES,
57
66
  SUBSCRIBER_ACCOUNT_CAPABILITY: () => SUBSCRIBER_ACCOUNT_CAPABILITY,
58
67
  SUBSCRIBER_ERROR_CODES: () => SUBSCRIBER_ERROR_CODES,
68
+ VERSION_RETIREMENT_CAPABILITY: () => VERSION_RETIREMENT_CAPABILITY,
59
69
  applyPromo: () => applyPromo,
60
70
  assertPersistenceCapabilities: () => assertPersistenceCapabilities,
61
71
  buildActivePlanVersionWhere: () => buildActivePlanVersionWhere,
62
72
  buildActiveVersionWhere: () => buildActiveVersionWhere,
63
73
  buildFeatureRequiresIndex: () => buildFeatureRequiresIndex,
64
74
  bundleDraftDefaults: () => bundleDraftDefaults,
75
+ bundleKeyTaken: () => bundleKeyTaken,
65
76
  bundleStemDefaults: () => bundleStemDefaults,
66
77
  canonicalJson: () => canonicalJson,
78
+ catalogDraftExists: () => catalogDraftExists,
79
+ catalogVersionAlreadyPublished: () => catalogVersionAlreadyPublished,
80
+ catalogVersionGone: () => catalogVersionGone,
67
81
  classifyBundleVersionDiff: () => classifyBundleVersionDiff,
68
82
  classifyIssuerChange: () => classifyIssuerChange,
69
83
  classifyPlanDiff: () => classifyPlanDiff,
84
+ classifyVersionOffer: () => classifyVersionOffer,
70
85
  collectUnsatisfiedRequires: () => collectUnsatisfiedRequires,
86
+ computeIncludedVat: () => computeIncludedVat,
87
+ concurrencyGate: () => concurrencyGate,
88
+ concurrencyGateOf: () => concurrencyGateOf,
71
89
  contractPartiesOf: () => contractPartiesOf,
72
90
  coverageExcludingSelf: () => coverageExcludingSelf,
91
+ customLimitsReader: () => customLimitsReader,
73
92
  definedFields: () => definedFields,
74
93
  diffSettings: () => diffSettings,
75
94
  formatCustomerNumber: () => formatCustomerNumber,
76
95
  formatErrorMessage: () => formatErrorMessage,
96
+ grossFromNet: () => grossFromNet,
77
97
  identityCorrectionDelta: () => identityCorrectionDelta,
78
98
  isBundleRedundant: () => isBundleRedundant,
79
99
  isForeignPaymentMethodReferenceError: () => isForeignPaymentMethodReferenceError,
100
+ isMaintenanceAnnouncementLapsed: () => isMaintenanceAnnouncementLapsed,
101
+ isMaintenanceOverrun: () => isMaintenanceOverrun,
80
102
  isPaymentCallbackRejectedError: () => isPaymentCallbackRejectedError,
103
+ isPersistenceRefusal: () => isPersistenceRefusal,
81
104
  isPlatformUserExistsError: () => isPlatformUserExistsError,
105
+ isVersionActiveAt: () => isVersionActiveAt,
82
106
  isVersionEditable: () => isVersionEditable,
83
107
  issuerIdentityOf: () => issuerIdentityOf,
84
108
  keepOneRecommended: () => keepOneRecommended,
109
+ maintenanceRetryAfterSeconds: () => maintenanceRetryAfterSeconds,
110
+ maintenanceStatusOf: () => maintenanceStatusOf,
111
+ maintenanceWindowStatusOf: () => maintenanceWindowStatusOf,
112
+ maintenanceWindowViewOf: () => maintenanceWindowViewOf,
113
+ marketingProjectionTaken: () => marketingProjectionTaken,
114
+ membersLeftOut: () => membersLeftOut,
85
115
  missingRequiresFor: () => missingRequiresFor,
86
116
  movedIdentityFields: () => movedIdentityFields,
117
+ netFromGross: () => netFromGross,
118
+ noActivePlanVersion: () => noActivePlanVersion,
119
+ parseZonedInstant: () => parseZonedInstant,
120
+ percentOf: () => percentOf,
87
121
  pickActivePromo: () => pickActivePromo,
88
122
  planCatalogSettingsOf: () => planCatalogSettingsOf,
123
+ planKeyTaken: () => planKeyTaken,
124
+ planNotInCatalog: () => planNotInCatalog,
89
125
  previousUtcDay: () => previousUtcDay,
126
+ promoCodeTaken: () => promoCodeTaken,
90
127
  promoStatus: () => promoStatus,
91
128
  promotionOnPrice: () => promotionOnPrice,
129
+ prorate: () => prorate,
130
+ readCustomLimits: () => readCustomLimits,
92
131
  readQuotaRecord: () => readQuotaRecord,
93
132
  readQuotaValue: () => readQuotaValue,
94
133
  recordedIssuerIdentity: () => recordedIssuerIdentity,
95
134
  refuseForeignPaymentMethodReference: () => refuseForeignPaymentMethodReference,
96
135
  resolveBundleAvailability: () => resolveBundleAvailability,
97
136
  resolveErrorMessage: () => resolveErrorMessage,
137
+ retirementCostsTheSubscription: () => retirementCostsTheSubscription,
138
+ retirementReminderDueAt: () => retirementReminderDueAt,
139
+ retirementReminderIsDue: () => retirementReminderIsDue,
140
+ retirementSwitchTerms: () => retirementSwitchTerms,
141
+ roundToCents: () => roundToCents,
98
142
  sameLegalIdentity: () => sameLegalIdentity,
143
+ scheduledChangeAfterWrite: () => scheduledChangeAfterWrite,
99
144
  selectChargeableBundles: () => selectChargeableBundles,
100
145
  settingsSubtreeOf: () => settingsSubtreeOf,
101
146
  startOfUtcDay: () => startOfUtcDay,
102
147
  subscriberChargeColumns: () => subscriberChargeColumns,
103
148
  subscriberFromRegistration: () => subscriberFromRegistration,
104
149
  subscriberPaymentMethodColumns: () => subscriberPaymentMethodColumns,
150
+ subscriptionBundleAlreadyCancelled: () => subscriptionBundleAlreadyCancelled,
151
+ subscriptionBundleGone: () => subscriptionBundleGone,
152
+ subscriptionContractGone: () => subscriptionContractGone,
153
+ subscriptionGone: () => subscriptionGone,
154
+ sumToCents: () => sumToCents,
105
155
  toBundleStemRow: () => toBundleStemRow,
156
+ toCents: () => toCents,
106
157
  toContractLineItemRecord: () => toContractLineItemRecord,
158
+ toDecimalString: () => toDecimalString,
159
+ toMaintenanceWindowRecord: () => toMaintenanceWindowRecord,
107
160
  toPlanRow: () => toPlanRow,
108
161
  toPlanVersionRow: () => toPlanVersionRow,
109
162
  toRunningContractIssuer: () => toRunningContractIssuer,
@@ -112,7 +165,12 @@ __export(index_exports, {
112
165
  toSubscriberPaymentMethodRecord: () => toSubscriberPaymentMethodRecord,
113
166
  toSubscriberRecord: () => toSubscriberRecord,
114
167
  toSubscriptionBundleRecord: () => toSubscriptionBundleRecord,
115
- toSubscriptionContractRecord: () => toSubscriptionContractRecord
168
+ toSubscriptionContractRecord: () => toSubscriptionContractRecord,
169
+ toSubscriptionNoticeRecord: () => toSubscriptionNoticeRecord,
170
+ toVersionRetirementRecord: () => toVersionRetirementRecord,
171
+ versionOnSale: () => versionOnSale,
172
+ versionOnSaleOrNext: () => versionOnSaleOrNext,
173
+ versionSale: () => versionSale
116
174
  });
117
175
  module.exports = __toCommonJS(index_exports);
118
176
 
@@ -129,7 +187,8 @@ function buildActivePlanVersionWhere(asOf, options = {}) {
129
187
  const asOfDayStart = startOfUtcDay(asOf);
130
188
  const validityWindow = [
131
189
  { OR: [{ validFrom: null }, { validFrom: { lte: asOf } }] },
132
- { OR: [{ validUntil: null }, { validUntil: { gte: asOfDayStart } }] }
190
+ { OR: [{ validUntil: null }, { validUntil: { gte: asOfDayStart } }] },
191
+ { OR: [{ supersededAt: null }, { validUntil: { not: null } }] }
133
192
  ];
134
193
  if (!options.withEndsAt) {
135
194
  return { publishedAt: { not: null }, AND: validityWindow };
@@ -139,6 +198,16 @@ function buildActivePlanVersionWhere(asOf, options = {}) {
139
198
  AND: [...validityWindow, { OR: [{ endsAt: null }, { endsAt: { gt: asOf } }] }]
140
199
  };
141
200
  }
201
+ function isVersionActiveAt(version, asOf) {
202
+ const at = (value) => value === null || value === void 0 ? null : new Date(value);
203
+ const validFrom = at(version.validFrom);
204
+ const validUntil = at(version.validUntil);
205
+ const endsAt = at(version.endsAt);
206
+ if (validFrom && validFrom > asOf) return false;
207
+ if (validUntil && validUntil < startOfUtcDay(asOf)) return false;
208
+ if (version.supersededAt && !validUntil) return false;
209
+ return !(endsAt && endsAt <= asOf);
210
+ }
142
211
  var buildActiveVersionWhere = buildActivePlanVersionWhere;
143
212
 
144
213
  // src/bundle-draft-defaults.ts
@@ -192,6 +261,79 @@ function definedFields(data, keys) {
192
261
  var MARKETING_PRIORITY_MIN = 0;
193
262
  var MARKETING_PRIORITY_MAX = 1e4;
194
263
 
264
+ // src/money.ts
265
+ var HUNDRED = { units: 100n, scale: 0 };
266
+ function exactDecimal(value) {
267
+ if (!Number.isFinite(value)) {
268
+ throw new RangeError(`${String(value)} is not an amount money can be computed from.`);
269
+ }
270
+ const text = String(value);
271
+ const exponentAt = text.indexOf("e");
272
+ const mantissa = exponentAt === -1 ? text : text.slice(0, exponentAt);
273
+ const exponent = exponentAt === -1 ? 0 : Number(text.slice(exponentAt + 1));
274
+ const pointAt = mantissa.indexOf(".");
275
+ const whole = pointAt === -1 ? mantissa : mantissa.slice(0, pointAt);
276
+ const fraction = pointAt === -1 ? "" : mantissa.slice(pointAt + 1);
277
+ let units = BigInt(whole + fraction);
278
+ let scale = fraction.length - exponent;
279
+ if (scale < 0) {
280
+ units *= 10n ** BigInt(-scale);
281
+ scale = 0;
282
+ }
283
+ return { units, scale };
284
+ }
285
+ function sum(a, b) {
286
+ const scale = Math.max(a.scale, b.scale);
287
+ return {
288
+ units: a.units * 10n ** BigInt(scale - a.scale) + b.units * 10n ** BigInt(scale - b.scale),
289
+ scale
290
+ };
291
+ }
292
+ function roundHalfAwayFromZero(numerator, denominator) {
293
+ if (denominator === 0n) throw new RangeError("An amount cannot be divided by zero.");
294
+ const negative = numerator < 0n !== denominator < 0n;
295
+ const n = numerator < 0n ? -numerator : numerator;
296
+ const d = denominator < 0n ? -denominator : denominator;
297
+ const quotient = n / d;
298
+ const rounded = n % d * 2n >= d ? quotient + 1n : quotient;
299
+ return negative ? -rounded : rounded;
300
+ }
301
+ function cents(amount, factor, divisor) {
302
+ const numerator = amount.units * factor.units * 10n ** BigInt(divisor.scale) * 100n;
303
+ const denominator = divisor.units * 10n ** BigInt(amount.scale + factor.scale);
304
+ return roundHalfAwayFromZero(numerator, denominator);
305
+ }
306
+ function amountOf(inCents) {
307
+ return Number(inCents) / 100;
308
+ }
309
+ var ONE = { units: 1n, scale: 0 };
310
+ function roundToCents(amount) {
311
+ return amountOf(cents(exactDecimal(amount), ONE, ONE));
312
+ }
313
+ function sumToCents(...terms) {
314
+ const total = terms.map(exactDecimal).reduce(sum, { units: 0n, scale: 0 });
315
+ return amountOf(cents(total, ONE, ONE));
316
+ }
317
+ function toCents(amount) {
318
+ return Number(cents(exactDecimal(amount), ONE, ONE));
319
+ }
320
+ function percentOf(amount, percent) {
321
+ return amountOf(cents(exactDecimal(amount), exactDecimal(percent), HUNDRED));
322
+ }
323
+ function prorate(amount, part, whole) {
324
+ return amountOf(cents(exactDecimal(amount), exactDecimal(part), exactDecimal(whole)));
325
+ }
326
+ function grossFromNet(net, vatRate) {
327
+ return amountOf(cents(exactDecimal(net), sum(HUNDRED, exactDecimal(vatRate)), HUNDRED));
328
+ }
329
+ function netFromGross(gross, vatRate) {
330
+ return amountOf(cents(exactDecimal(gross), HUNDRED, sum(HUNDRED, exactDecimal(vatRate))));
331
+ }
332
+ function computeIncludedVat(gross, vatRate) {
333
+ const rate = exactDecimal(vatRate);
334
+ return amountOf(cents(exactDecimal(gross), rate, sum(HUNDRED, rate)));
335
+ }
336
+
195
337
  // src/promotion.types.ts
196
338
  function promoStatus(promo, today = /* @__PURE__ */ new Date()) {
197
339
  const from = new Date(promo.validFrom);
@@ -214,7 +356,9 @@ function boundedPromo(promo, basePrice) {
214
356
  const pct = withinBounds(promo.value, 100);
215
357
  return {
216
358
  kind: "percent",
217
- discounted: Math.round(basePrice * (100 - pct)) / 100,
359
+ // The discount is rounded and taken off, as a promo code's is: two
360
+ // rules for one percentage would state two prices for it.
361
+ discounted: sumToCents(basePrice, -percentOf(basePrice, pct)),
218
362
  original: basePrice,
219
363
  pct
220
364
  };
@@ -223,7 +367,7 @@ function boundedPromo(promo, basePrice) {
223
367
  const saved = withinBounds(promo.value, basePrice);
224
368
  return {
225
369
  kind: "amount",
226
- discounted: Math.round((basePrice - saved) * 100) / 100,
370
+ discounted: sumToCents(basePrice, -saved),
227
371
  original: basePrice,
228
372
  saved
229
373
  };
@@ -401,6 +545,286 @@ function appendNullableDecimalChange(out, field, oldValue, newValue, polarity) {
401
545
  appendDecimalChange(out, field, oldValue, newValue, polarity);
402
546
  }
403
547
 
548
+ // src/version-offer.ts
549
+ function takesSomethingAway(change) {
550
+ if (change.direction !== "REGRESSION") return false;
551
+ return change.field === "features.removed" || change.field.startsWith("quotas.");
552
+ }
553
+ function classifyVersionOffer(bound, candidate) {
554
+ const { changes } = classifyBundleVersionDiff(bound, candidate);
555
+ if (changes.some(takesSomethingAway)) return { class: "takes-something-away", changes };
556
+ if (changes.some((change) => change.direction === "REGRESSION")) {
557
+ return { class: "more-for-more", changes };
558
+ }
559
+ return { class: changes.length > 0 ? "improvement" : "same", changes };
560
+ }
561
+
562
+ // src/maintenance-window.types.ts
563
+ var MAINTENANCE_MESSAGE_MAX_LENGTH = 500;
564
+ var MAINTENANCE_STATE_MAX_AGE_SECONDS = 5;
565
+ var MAINTENANCE_CAPABILITY = "maintenance.manage";
566
+
567
+ // src/closed-value.ts
568
+ function oneOf(allowed, value, source) {
569
+ const match = allowed.find((entry) => entry === value);
570
+ if (match === void 0) {
571
+ throw new Error(
572
+ `${source.table} row '${source.id}' holds ${source.column} '${value}', which is none of ${allowed.join(", ")}.`
573
+ );
574
+ }
575
+ return match;
576
+ }
577
+
578
+ // src/subscription-notice-mapping.ts
579
+ var TABLE = "subscription_notices";
580
+ var KINDS = {
581
+ "version-offered": true,
582
+ "version-retired": true,
583
+ "version-retirement-reminder": true
584
+ };
585
+ function toSubscriptionNoticeRecord(row) {
586
+ return {
587
+ id: row.id,
588
+ tenantId: row.tenantId,
589
+ subscriptionId: row.subscriptionId,
590
+ kind: oneOf(Object.keys(KINDS), row.kind, {
591
+ table: TABLE,
592
+ column: "kind",
593
+ id: row.id
594
+ }),
595
+ subject: row.subject,
596
+ content: row.content,
597
+ createdAt: row.createdAt,
598
+ claimedAt: row.claimedAt,
599
+ deliveredAt: row.deliveredAt,
600
+ delivery: row.deliveredAt === null ? null : deliveryOf(row)
601
+ };
602
+ }
603
+ function deliveryOf(row) {
604
+ const { recipients, channel } = row;
605
+ if (!Array.isArray(recipients) || !recipients.every((recipient) => typeof recipient === "string") || channel === null) {
606
+ throw new Error(
607
+ `${TABLE} row '${row.id}' is delivered, but its recipients are not a list of strings or its channel is missing.`
608
+ );
609
+ }
610
+ return { recipients: [...recipients], channel };
611
+ }
612
+
613
+ // src/version-sale.ts
614
+ var DAY_MS = 24 * 60 * 60 * 1e3;
615
+ function versionSale(version, now) {
616
+ if (!version.publishedAt) return { kind: "draft" };
617
+ if (isVersionActiveAt(version, now)) {
618
+ return { kind: "on-sale", until: lastDayOnSale(version) };
619
+ }
620
+ const start = dateOf(version.validFrom);
621
+ if (start && start > now && isVersionActiveAt(version, start)) {
622
+ return { kind: "scheduled", from: dayOf(start) };
623
+ }
624
+ return { kind: "off-sale", since: firstDayOffSale(version, now) };
625
+ }
626
+ function versionOnSale(versions, now) {
627
+ const onSale = versions.filter((v) => versionSale(v, now).kind === "on-sale");
628
+ const startOf = (v) => dateOf(v.validFrom)?.getTime() ?? -Infinity;
629
+ onSale.sort((a, b) => startOf(b) - startOf(a) || (b.version ?? 0) - (a.version ?? 0));
630
+ return onSale[0] ?? null;
631
+ }
632
+ function versionOnSaleOrNext(versions, now) {
633
+ const onSale = versionOnSale(versions, now);
634
+ if (onSale) return onSale;
635
+ const scheduled = versions.filter((v) => versionSale(v, now).kind === "scheduled");
636
+ const startOf = (v) => dateOf(v.validFrom)?.getTime() ?? Infinity;
637
+ scheduled.sort((a, b) => startOf(a) - startOf(b));
638
+ return scheduled[0] ?? null;
639
+ }
640
+ function lastDayOnSale(version) {
641
+ const days = [];
642
+ const validUntil = dateOf(version.validUntil);
643
+ if (validUntil) days.push(dayOf(validUntil));
644
+ const endsAt = dateOf(version.endsAt);
645
+ if (endsAt) days.push(dayOf(new Date(endsAt.getTime() - 1)));
646
+ return earliest(days);
647
+ }
648
+ function firstDayOffSale(version, now) {
649
+ const days = [];
650
+ const validUntil = dateOf(version.validUntil);
651
+ if (validUntil && dayOf(validUntil) < dayOf(now)) {
652
+ days.push(dayOf(new Date(validUntil.getTime() + DAY_MS)));
653
+ }
654
+ const endsAt = dateOf(version.endsAt);
655
+ if (endsAt && endsAt <= now) days.push(dayOf(new Date(endsAt.getTime() - 1 + DAY_MS)));
656
+ return earliest(days);
657
+ }
658
+ function earliest(days) {
659
+ return days.length === 0 ? null : days.reduce((a, b) => a < b ? a : b);
660
+ }
661
+ function dateOf(value) {
662
+ if (value === null || value === void 0) return null;
663
+ const date = new Date(value);
664
+ return Number.isNaN(date.getTime()) ? null : date;
665
+ }
666
+ function dayOf(date) {
667
+ return date.toISOString().slice(0, 10);
668
+ }
669
+
670
+ // src/version-retirement.types.ts
671
+ var VERSION_RETIREMENT_CAPABILITY = "planVersions.retire";
672
+
673
+ // src/version-retirement-mapping.ts
674
+ function toVersionRetirementRecord(row) {
675
+ return {
676
+ id: row.id,
677
+ retired: {
678
+ planVersionId: row.retiredPlanVersionId,
679
+ planKey: row.retiredPlanKey,
680
+ version: row.retiredVersion
681
+ },
682
+ replacement: {
683
+ planVersionId: row.replacementPlanVersionId,
684
+ planKey: row.replacementPlanKey,
685
+ version: row.replacementVersion
686
+ },
687
+ announcedAt: row.announcedAt,
688
+ announcedBy: row.announcedBy
689
+ };
690
+ }
691
+
692
+ // src/scheduled-change-after-write.ts
693
+ var NO_SCHEDULED_CHANGE = {
694
+ pendingPlan: null,
695
+ pendingBillingCycle: null,
696
+ pendingEffectiveAt: null,
697
+ pendingChangeVersionId: null
698
+ };
699
+ function scheduledChangeAfterWrite(current, input) {
700
+ if (!input.keepsPendingChange) return NO_SCHEDULED_CHANGE;
701
+ const movesOnlyTheRhythm = current.pendingPlan === current.plan && current.pendingChangeVersionId === null;
702
+ return movesOnlyTheRhythm && current.plan !== input.planId ? { pendingPlan: input.planId } : {};
703
+ }
704
+
705
+ // src/retirement-switch.ts
706
+ function retirementSwitchTerms(notice, billingCycle) {
707
+ const priceIn = (side) => billingCycle === "YEARLY" ? side.yearlyNet : side.monthlyNet;
708
+ const replacement = priceIn(notice.replacement);
709
+ if (replacement === null) return null;
710
+ const retired = priceIn(notice.retired);
711
+ if (retired === null || replacement <= retired) return { priceNet: replacement, held: null };
712
+ return {
713
+ priceNet: replacement,
714
+ held: {
715
+ priceNet: retired,
716
+ amountNet: sumToCents(replacement, -retired),
717
+ // The last day to cancel without notice is the same day: the last
718
+ // whole one before the date the replacement takes over.
719
+ lastDay: notice.lastDayToCancel
720
+ }
721
+ };
722
+ }
723
+
724
+ // src/retirement-reminder.ts
725
+ var RETIREMENT_REMINDER_LEAD_DAYS = 14;
726
+ var DAY_MS2 = 24 * 60 * 60 * 1e3;
727
+ function retirementReminderDueAt(notice) {
728
+ return new Date(Date.parse(notice.effectiveAt) - RETIREMENT_REMINDER_LEAD_DAYS * DAY_MS2);
729
+ }
730
+ function retirementReminderIsDue(notice, now) {
731
+ return retirementReminderDueAt(notice) <= now && now.getTime() < Date.parse(notice.effectiveAt);
732
+ }
733
+ function retirementCostsTheSubscription(notice, billingCycle) {
734
+ if (notice.changes.some(takesSomethingAway)) return true;
735
+ const terms = retirementSwitchTerms(notice, billingCycle);
736
+ return terms === null || terms.held !== null;
737
+ }
738
+
739
+ // src/maintenance-window-mapping.ts
740
+ function toMaintenanceWindowRecord(row) {
741
+ return {
742
+ id: row.id,
743
+ startsAt: row.startsAt,
744
+ endsAt: row.endsAt,
745
+ message: row.message,
746
+ createdAt: row.createdAt,
747
+ createdBy: row.createdBy,
748
+ lockedAt: row.lockedAt,
749
+ lockedBy: row.lockedBy,
750
+ endedAt: row.endedAt,
751
+ endedBy: row.endedBy
752
+ };
753
+ }
754
+
755
+ // src/maintenance-window-views.ts
756
+ var RETRY_AFTER_WITHOUT_END_SECONDS = 60;
757
+ var iso = (date) => date ? date.toISOString() : null;
758
+ function maintenanceWindowStatusOf(window) {
759
+ if (window.endedAt) return window.lockedAt ? "ended" : "cancelled";
760
+ return window.lockedAt ? "locked" : "announced";
761
+ }
762
+ function isMaintenanceOverrun(window, now) {
763
+ return maintenanceWindowStatusOf(window) === "locked" && window.endsAt !== null && window.endsAt.getTime() <= now.getTime();
764
+ }
765
+ function isMaintenanceAnnouncementLapsed(window, now) {
766
+ return maintenanceWindowStatusOf(window) === "announced" && window.endsAt !== null && window.endsAt.getTime() <= now.getTime();
767
+ }
768
+ function maintenanceStatusOf(open, now) {
769
+ if (!open) return { state: "none" };
770
+ const status = maintenanceWindowStatusOf(open);
771
+ if (status === "locked" && open.lockedAt) {
772
+ return {
773
+ state: "locked",
774
+ lockedAt: open.lockedAt.toISOString(),
775
+ endsAt: iso(open.endsAt),
776
+ message: open.message,
777
+ overrun: isMaintenanceOverrun(open, now)
778
+ };
779
+ }
780
+ if (status === "announced" && open.startsAt && open.endsAt && !isMaintenanceAnnouncementLapsed(open, now)) {
781
+ return {
782
+ state: "announced",
783
+ startsAt: open.startsAt.toISOString(),
784
+ endsAt: open.endsAt.toISOString(),
785
+ message: open.message
786
+ };
787
+ }
788
+ return { state: "none" };
789
+ }
790
+ function maintenanceWindowViewOf(window, now) {
791
+ return {
792
+ id: window.id,
793
+ status: maintenanceWindowStatusOf(window),
794
+ startsAt: iso(window.startsAt),
795
+ endsAt: iso(window.endsAt),
796
+ message: window.message,
797
+ createdAt: window.createdAt.toISOString(),
798
+ createdBy: window.createdBy,
799
+ lockedAt: iso(window.lockedAt),
800
+ lockedBy: window.lockedBy,
801
+ endedAt: iso(window.endedAt),
802
+ endedBy: window.endedBy,
803
+ overrun: isMaintenanceOverrun(window, now),
804
+ lapsed: isMaintenanceAnnouncementLapsed(window, now)
805
+ };
806
+ }
807
+ function maintenanceRetryAfterSeconds(window, now) {
808
+ if (!window.endsAt || window.endsAt.getTime() <= now.getTime()) {
809
+ return RETRY_AFTER_WITHOUT_END_SECONDS;
810
+ }
811
+ return Math.max(1, Math.ceil((window.endsAt.getTime() - now.getTime()) / 1e3));
812
+ }
813
+
814
+ // src/zoned-instant.ts
815
+ function endsInOffset(text) {
816
+ const sign = text.charAt(text.length - 6);
817
+ return (sign === "+" || sign === "-") && text.charAt(text.length - 3) === ":";
818
+ }
819
+ function parseZonedInstant(text) {
820
+ const value = text.trim();
821
+ if (!value.includes("T")) return null;
822
+ const zoned = value.endsWith("Z") || value.endsWith("z") || endsInOffset(value);
823
+ if (!zoned) return null;
824
+ const moment = new Date(value);
825
+ return Number.isNaN(moment.getTime()) ? null : moment;
826
+ }
827
+
404
828
  // src/settings-subtree.ts
405
829
  var CATALOGUE_KEYS = /* @__PURE__ */ new Set([
406
830
  "schemaVersion",
@@ -518,11 +942,42 @@ var CATALOG_ERROR_CODES = {
518
942
  PLAN_TERMINATE_INVALID_DATE: "PLAN_TERMINATE_INVALID_DATE",
519
943
  PLAN_TERMINATE_DATE_NOT_FUTURE: "PLAN_TERMINATE_DATE_NOT_FUTURE",
520
944
  PLAN_TERMINATE_NOT_IMPLEMENTED: "PLAN_TERMINATE_NOT_IMPLEMENTED",
945
+ /**
946
+ * Subscriptions told of a retirement still move onto this version, and
947
+ * nothing moves onto a version that has ended. Carries the version, its
948
+ * plan key and the first day it may end: the day after the last move.
949
+ */
950
+ PLAN_TERMINATE_BEFORE_RETIREMENT_MOVES: "PLAN_TERMINATE_BEFORE_RETIREMENT_MOVES",
951
+ /**
952
+ * Subscriptions told of a retirement are past their date and still to be
953
+ * moved onto this version — a move the platform could not make yet — so it
954
+ * cannot end until they have moved. Carries how many, the version and its
955
+ * plan key.
956
+ */
957
+ PLAN_TERMINATE_WHILE_MOVES_OVERDUE: "PLAN_TERMINATE_WHILE_MOVES_OVERDUE",
958
+ /**
959
+ * Subscriptions on a retired version have not yet been told of their move
960
+ * onto this version — their notice has reached nobody, so their date is not
961
+ * set — and it cannot end until they have been. Carries how many, the
962
+ * version and its plan key.
963
+ */
964
+ PLAN_TERMINATE_WHILE_NOTICES_UNDELIVERED: "PLAN_TERMINATE_WHILE_NOTICES_UNDELIVERED",
521
965
  // ── bundle versions ──
522
966
  BUNDLE_VERSION_ALREADY_PUBLISHED: "BUNDLE_VERSION_ALREADY_PUBLISHED",
523
967
  BUNDLE_VERSION_NOT_EDITABLE: "BUNDLE_VERSION_NOT_EDITABLE",
524
968
  BUNDLE_VERSION_NOT_PUBLISHED: "BUNDLE_VERSION_NOT_PUBLISHED",
525
969
  BUNDLE_VERSION_SUPERSEDED: "BUNDLE_VERSION_SUPERSEDED",
970
+ /**
971
+ * The bundle version is published, and its window has not opened: it goes
972
+ * on sale on `validFrom`, and until then its predecessor is the one sold.
973
+ */
974
+ BUNDLE_VERSION_NOT_YET_ON_SALE: "BUNDLE_VERSION_NOT_YET_ON_SALE",
975
+ /**
976
+ * The add-on itself has been deleted from the catalogue. Deleting it leaves
977
+ * its versions' dates as they were, so the version asked for can still be
978
+ * inside its window; it is not on offer all the same.
979
+ */
980
+ BUNDLE_DELETED: "BUNDLE_DELETED",
526
981
  BUNDLE_VERSION_REGRESSION: "BUNDLE_VERSION_REGRESSION",
527
982
  BUNDLE_VERSION_ZERO_PRICE: "BUNDLE_VERSION_ZERO_PRICE",
528
983
  BUNDLE_VERSION_NO_PRICE: "BUNDLE_VERSION_NO_PRICE",
@@ -581,6 +1036,22 @@ var BILLING_ERROR_CODES = {
581
1036
  BUNDLE_NOT_SELF_SERVICE: "BUNDLE_NOT_SELF_SERVICE",
582
1037
  BUNDLE_CYCLE_EXCEEDS_PLAN: "BUNDLE_CYCLE_EXCEEDS_PLAN",
583
1038
  BUNDLE_NOT_PRICED_FOR_THIS_PLAN: "BUNDLE_NOT_PRICED_FOR_THIS_PLAN",
1039
+ /**
1040
+ * The bundle cannot run on a plan the subscription is already set to move
1041
+ * to — by a scheduled change, or by a retirement it has been told of — and
1042
+ * the booking would still be running then: that plan does not book it, or
1043
+ * has no price for it. Carries that plan's key, the rhythm it is billed in
1044
+ * there, and the day the move takes effect from, which can lie in the past
1045
+ * while the move has not run yet.
1046
+ */
1047
+ BUNDLE_CANNOT_RUN_ON_UPCOMING_PLAN: "BUNDLE_CANNOT_RUN_ON_UPCOMING_PLAN",
1048
+ /**
1049
+ * As `BUNDLE_CANNOT_RUN_ON_UPCOMING_PLAN`, where only the rhythm stands in
1050
+ * the way: a yearly bundle, and the subscription billed monthly from the
1051
+ * day of the move — a switch of rhythm on the plan it keeps, for one, where
1052
+ * naming the plan would point at the one it is already on. Same values.
1053
+ */
1054
+ BUNDLE_CANNOT_RUN_ON_UPCOMING_CYCLE: "BUNDLE_CANNOT_RUN_ON_UPCOMING_CYCLE",
584
1055
  SUBSCRIPTION_BUNDLE_ALREADY_CANCELLED: "SUBSCRIPTION_BUNDLE_ALREADY_CANCELLED",
585
1056
  SUBSCRIPTION_BUNDLE_NOT_CANCELLED: "SUBSCRIPTION_BUNDLE_NOT_CANCELLED",
586
1057
  SUBSCRIPTION_BUNDLE_CANCELLATION_EFFECTIVE: "SUBSCRIPTION_BUNDLE_CANCELLATION_EFFECTIVE",
@@ -600,6 +1071,12 @@ var BILLING_ERROR_CODES = {
600
1071
  * in it: a plan without a yearly price is a monthly plan.
601
1072
  */
602
1073
  PLAN_NOT_SOLD_IN_CYCLE: "PLAN_NOT_SOLD_IN_CYCLE",
1074
+ /**
1075
+ * The subscription is bound to a plan version the plan repository does not
1076
+ * find, or finds as a version of another plan. What the subscription pays
1077
+ * cannot be read, so no change is quoted until the binding is repaired.
1078
+ */
1079
+ BOUND_PLAN_VERSION_UNREADABLE: "BOUND_PLAN_VERSION_UNREADABLE",
603
1080
  /** Plan change refused. Carries `blockers[]` with their own codes. */
604
1081
  PLAN_CHANGE_BLOCKED: "PLAN_CHANGE_BLOCKED",
605
1082
  /**
@@ -622,6 +1099,103 @@ var BILLING_ERROR_CODES = {
622
1099
  * Carries the recomputed dates, so the page can re-ask instead of guessing.
623
1100
  */
624
1101
  CANCELLATION_TERMS_CHANGED: "CANCELLATION_TERMS_CHANGED",
1102
+ /**
1103
+ * The version the caller asked to switch to is no longer the one offered —
1104
+ * another is on sale, the subscription moved, or there is no offer. Nothing
1105
+ * was switched. Carries the current `offer`, or `null`, so the page can show
1106
+ * it instead of guessing.
1107
+ */
1108
+ VERSION_OFFER_CHANGED: "VERSION_OFFER_CHANGED",
1109
+ /**
1110
+ * The version a change to another plan named is not the one on sale when
1111
+ * the change was submitted: its preview was shown at another moment.
1112
+ * Nothing was changed. Carries the current `preview`, so the page shows
1113
+ * what the change binds now instead of what it would have.
1114
+ */
1115
+ PLAN_CHANGE_QUOTE_CHANGED: "PLAN_CHANGE_QUOTE_CHANGED",
1116
+ /**
1117
+ * A change to another plan named no version, where its preview names one:
1118
+ * nothing says which price and terms the customer was shown, so nothing
1119
+ * is changed.
1120
+ */
1121
+ PLAN_CHANGE_VERSION_NOT_NAMED: "PLAN_CHANGE_VERSION_NOT_NAMED",
1122
+ /**
1123
+ * A version that takes something away takes effect when the term ends, and
1124
+ * the subscription's cancellation lands no later: the switch would never
1125
+ * happen, so it is refused rather than recorded. Carries both dates.
1126
+ */
1127
+ VERSION_SWITCH_AFTER_CANCELLATION: "VERSION_SWITCH_AFTER_CANCELLATION",
1128
+ /**
1129
+ * A version that takes something away takes effect when the term ends, and
1130
+ * it stops being sold before then — by its window or because an operator
1131
+ * ended it — so it could not be bound when the change comes due. Refused
1132
+ * rather than recorded; carries the dates.
1133
+ */
1134
+ VERSION_ENDS_BEFORE_SWITCH: "VERSION_ENDS_BEFORE_SWITCH",
1135
+ // ── retiring a version for running subscriptions ──
1136
+ /**
1137
+ * The operator's terms are not confirmed to carry the clause a retirement
1138
+ * rests on (`tenantBilling.orderlyRetirement.termsConfirmed` in
1139
+ * `config/saas.yaml`), so none is accepted.
1140
+ */
1141
+ RETIREMENT_TERMS_NOT_CONFIRMED: "RETIREMENT_TERMS_NOT_CONFIRMED",
1142
+ /**
1143
+ * The version is still on sale: a subscription booked after the
1144
+ * announcement would be on it without having been told. Carries the plan
1145
+ * key and the version.
1146
+ */
1147
+ RETIREMENT_VERSION_ON_SALE: "RETIREMENT_VERSION_ON_SALE",
1148
+ /** The replacement is not on sale, so nobody can continue on it. Carries plan key and version. */
1149
+ RETIREMENT_REPLACEMENT_NOT_ON_SALE: "RETIREMENT_REPLACEMENT_NOT_ON_SALE",
1150
+ /**
1151
+ * The replacement has no price in the rhythm some of the subscriptions it
1152
+ * reaches are billed in, so they cannot continue on it. Carries how many,
1153
+ * and the replacement's plan key and version.
1154
+ */
1155
+ RETIREMENT_REPLACEMENT_NOT_SOLD_IN_RHYTHM: "RETIREMENT_REPLACEMENT_NOT_SOLD_IN_RHYTHM",
1156
+ /**
1157
+ * Some of the subscriptions the retirement reaches hold an add-on that
1158
+ * cannot run on the replacement's plan, so they cannot continue on it.
1159
+ * Carries how many, and the replacement's plan key and version.
1160
+ */
1161
+ RETIREMENT_REPLACEMENT_CANNOT_CARRY_BUNDLES: "RETIREMENT_REPLACEMENT_CANNOT_CARRY_BUNDLES",
1162
+ /** The replacement named is the version being retired. */
1163
+ RETIREMENT_REPLACEMENT_IS_RETIRED: "RETIREMENT_REPLACEMENT_IS_RETIRED",
1164
+ /** No running subscription is on the version: there is nobody to tell. Carries plan key and version. */
1165
+ RETIREMENT_NOTHING_AFFECTED: "RETIREMENT_NOTHING_AFFECTED",
1166
+ /**
1167
+ * Some of the subscriptions it reaches were reached by a retirement within
1168
+ * the last twelve months, and a subscription is reached at most once in that
1169
+ * time. Carries how many and which.
1170
+ */
1171
+ RETIREMENT_WITHIN_TWELVE_MONTHS: "RETIREMENT_WITHIN_TWELVE_MONTHS",
1172
+ /**
1173
+ * The subscriptions the retirement reaches, or their dates, changed since
1174
+ * the preview the operator confirmed. Carries the preview as it stands.
1175
+ */
1176
+ RETIREMENT_PREVIEW_CHANGED: "RETIREMENT_PREVIEW_CHANGED",
1177
+ /** No retirement of the subscription's version waits for its date: there is nothing to switch to. */
1178
+ RETIREMENT_SWITCH_NOT_PENDING: "RETIREMENT_SWITCH_NOT_PENDING",
1179
+ /** The switch opens once the trial has converted: a trial has no contract to hold the price on. */
1180
+ RETIREMENT_SWITCH_IN_TRIAL: "RETIREMENT_SWITCH_IN_TRIAL",
1181
+ /**
1182
+ * An add-on running today cannot run beside the replacement's plan, which
1183
+ * the switch would move the subscription onto today. Carries the add-on,
1184
+ * the plan and the earliest day the booking can end. Its own code rather
1185
+ * than a plan change's: the switch has no other plan to choose and no day
1186
+ * to pick, so the way past it is the add-on ending.
1187
+ */
1188
+ RETIREMENT_SWITCH_BUNDLE_CANNOT_FOLLOW: "RETIREMENT_SWITCH_BUNDLE_CANNOT_FOLLOW",
1189
+ /**
1190
+ * The subscription takes no switch now: a change is scheduled, it has
1191
+ * ended, or its plan is held for a special contract.
1192
+ */
1193
+ RETIREMENT_SWITCH_NOT_OPEN: "RETIREMENT_SWITCH_NOT_OPEN",
1194
+ /**
1195
+ * The version named is not the replacement the retirement names. Carries
1196
+ * the retirement as it now stands.
1197
+ */
1198
+ RETIREMENT_SWITCH_CHANGED: "RETIREMENT_SWITCH_CHANGED",
625
1199
  // ── the preview routes' own blockers and warnings ──
626
1200
  //
627
1201
  // These travel inside a 200 response, in `blockers[]` and `warnings[]`, not
@@ -651,14 +1225,23 @@ var BILLING_ERROR_CODES = {
651
1225
  CANCELLATION_LOCKS_THE_CYCLE: "CANCELLATION_LOCKS_THE_CYCLE",
652
1226
  /**
653
1227
  * A bundle the tenant already holds runs past the cycle they are moving to.
1228
+ * Carries the bundle, the earliest day the booking can end, and the two
1229
+ * rhythms.
654
1230
  *
655
1231
  * Its own code rather than `BUNDLE_CYCLE_EXCEEDS_PLAN`, which states the
656
1232
  * same rule about a booking that has not been made yet. The two need
657
1233
  * different sentences: this one can name the day the obstacle lifts and
658
- * tell the reader to cancel the booking, and that advice is wrong for
659
- * someone who is only about to book. One template cannot serve both.
1234
+ * tell the reader how to get past it, and that advice is wrong for someone
1235
+ * who is only about to book. One template cannot serve both.
660
1236
  */
661
1237
  BUNDLE_BOOKING_OUTLASTS_TARGET_CYCLE: "BUNDLE_BOOKING_OUTLASTS_TARGET_CYCLE",
1238
+ /**
1239
+ * A bundle the tenant already holds cannot run on the plan they are moving
1240
+ * to: that plan may not book it, or it has no price there in the rhythm
1241
+ * the booking is billed in. Carries the bundle, the plan and the earliest
1242
+ * day the booking can end.
1243
+ */
1244
+ BUNDLE_BOOKING_DOES_NOT_FIT_TARGET_PLAN: "BUNDLE_BOOKING_DOES_NOT_FIT_TARGET_PLAN",
662
1245
  /**
663
1246
  * Features of the previewed bundle are already covered by the plan or by
664
1247
  * another booked bundle. A warning rather than a blocker: paying twice is
@@ -680,7 +1263,6 @@ var BILLING_ERROR_CODES = {
680
1263
  * tenant reads and therefore needs a shipped text for.
681
1264
  */
682
1265
  BUNDLE_FEATURE_DEPENDENCY_UNSATISFIED: "BUNDLE_FEATURE_DEPENDENCY_UNSATISFIED",
683
- NO_PENDING_PLAN_VERSION: "NO_PENDING_PLAN_VERSION",
684
1266
  ONBOARDING_CREATE_FAILED: "ONBOARDING_CREATE_FAILED",
685
1267
  BUNDLE_PREVIEW_ARGUMENT_AMBIGUOUS: "BUNDLE_PREVIEW_ARGUMENT_AMBIGUOUS",
686
1268
  /**
@@ -731,7 +1313,15 @@ var CONTRACT_ERROR_CODES = {
731
1313
  CHECKOUT_OFFER_CHANGED: "CHECKOUT_OFFER_CHANGED",
732
1314
  SUBSCRIPTION_CONTRACT_NOT_FOUND: "SUBSCRIPTION_CONTRACT_NOT_FOUND",
733
1315
  NO_ACTIVE_SUBSCRIPTION_CONTRACT: "NO_ACTIVE_SUBSCRIPTION_CONTRACT",
734
- SUBSCRIPTION_CONTRACT_ALREADY_CLOSED: "SUBSCRIPTION_CONTRACT_ALREADY_CLOSED"
1316
+ SUBSCRIPTION_CONTRACT_ALREADY_CLOSED: "SUBSCRIPTION_CONTRACT_ALREADY_CLOSED",
1317
+ /**
1318
+ * A successor would run beside another contract of the tenant: the
1319
+ * contract in force moved while it was being written — another successor
1320
+ * took its place, or a cancellation capped it — and kept moving on a
1321
+ * second attempt, or a contract begins after the moment the successor
1322
+ * would take effect. Nothing was written.
1323
+ */
1324
+ SUBSCRIPTION_CONTRACT_CHANGED: "SUBSCRIPTION_CONTRACT_CHANGED"
735
1325
  };
736
1326
  var SUBSCRIBER_ERROR_CODES = {
737
1327
  /**
@@ -842,6 +1432,24 @@ var SETTINGS_ERROR_CODES = {
842
1432
  /** No recorded change has this id, or the installation keeps no record at all. */
843
1433
  SETTINGS_CHANGE_NOT_FOUND: "SETTINGS_CHANGE_NOT_FOUND"
844
1434
  };
1435
+ var MAINTENANCE_ERROR_CODES = {
1436
+ /** The application is locked for maintenance; the body carries the window. */
1437
+ MAINTENANCE: "MAINTENANCE",
1438
+ /** A window is already open: move or cancel it before announcing another. */
1439
+ MAINTENANCE_WINDOW_ALREADY_OPEN: "MAINTENANCE_WINDOW_ALREADY_OPEN",
1440
+ /** No open window has this id — it ended, or it never existed. */
1441
+ MAINTENANCE_WINDOW_NOT_OPEN: "MAINTENANCE_WINDOW_NOT_OPEN",
1442
+ /** The window is locked: its start is history, and unlocking is what ends it. */
1443
+ MAINTENANCE_WINDOW_LOCKED: "MAINTENANCE_WINDOW_LOCKED",
1444
+ /** The announced end is not after the announced start. */
1445
+ MAINTENANCE_WINDOW_END_NOT_AFTER_START: "MAINTENANCE_WINDOW_END_NOT_AFTER_START",
1446
+ /** The announced end has already passed. */
1447
+ MAINTENANCE_WINDOW_END_IN_PAST: "MAINTENANCE_WINDOW_END_IN_PAST",
1448
+ /** A time without its zone, or not a date and time at all; `field` names which. */
1449
+ MAINTENANCE_TIME_INVALID: "MAINTENANCE_TIME_INVALID",
1450
+ /** The message is longer than `MAINTENANCE_MESSAGE_MAX_LENGTH`. */
1451
+ MAINTENANCE_MESSAGE_TOO_LONG: "MAINTENANCE_MESSAGE_TOO_LONG"
1452
+ };
845
1453
  var PLATFORM_ERROR_CODES = {
846
1454
  ...SETUP_ERROR_CODES,
847
1455
  ...AUTH_ERROR_CODES,
@@ -852,7 +1460,8 @@ var PLATFORM_ERROR_CODES = {
852
1460
  ...SUBSCRIBER_ERROR_CODES,
853
1461
  ...REGISTRATION_ERROR_CODES,
854
1462
  ...PAYMENT_ERROR_CODES,
855
- ...SETTINGS_ERROR_CODES
1463
+ ...SETTINGS_ERROR_CODES,
1464
+ ...MAINTENANCE_ERROR_CODES
856
1465
  };
857
1466
 
858
1467
  // src/errors.ts
@@ -871,121 +1480,555 @@ var PlatformUserExistsError = class extends Error {
871
1480
  function isPlatformUserExistsError(err) {
872
1481
  return err instanceof Error && err.code === USER_ALREADY_EXISTS;
873
1482
  }
874
-
875
- // src/feature-requires.ts
876
- function buildFeatureRequiresIndex(features) {
877
- const index = /* @__PURE__ */ new Map();
878
- for (const feature of features) {
879
- const requires = (feature.requires ?? []).filter((key) => key !== feature.featureKey);
880
- if (requires.length > 0) index.set(feature.featureKey, requires);
881
- }
882
- return index;
883
- }
884
- function collectUnsatisfiedRequires(selected, index) {
885
- const own = new Set(selected);
886
- const unsatisfied = /* @__PURE__ */ new Set();
887
- for (const featureKey of selected) {
888
- for (const required of index.get(featureKey) ?? []) {
889
- if (!own.has(required)) unsatisfied.add(required);
890
- }
891
- }
892
- return [...unsatisfied].sort((a, b) => a.localeCompare(b));
893
- }
894
- function missingRequiresFor(bundle, coveredFeatures) {
895
- const missing = (bundle.requiresFeatures ?? []).filter((key) => !coveredFeatures.has(key));
896
- return [...new Set(missing)].sort((a, b) => a.localeCompare(b));
897
- }
898
- function resolveBundleAvailability(bundle, coveredFeatures) {
899
- if (bundle.features.length > 0 && bundle.features.every((f) => coveredFeatures.has(f))) {
900
- return "covered";
901
- }
902
- if (missingRequiresFor(bundle, coveredFeatures).length > 0) return "missing-requires";
903
- return "bookable";
904
- }
905
- function coverageExcludingSelf(selfVersionId, planFeatures, selectedBundles) {
906
- const covered = new Set(planFeatures);
907
- for (const bundle of selectedBundles) {
908
- if (bundle.bundleVersionId === selfVersionId) continue;
909
- for (const feature of bundle.features) covered.add(feature);
1483
+ var PERSISTENCE_REFUSAL = "PersistenceRefusal";
1484
+ var PersistenceRefusal = class extends Error {
1485
+ constructor(code, reason, message, params = {}) {
1486
+ super(message);
1487
+ this.code = code;
1488
+ this.reason = reason;
1489
+ this.params = params;
1490
+ this.name = PERSISTENCE_REFUSAL;
910
1491
  }
911
- return covered;
912
- }
913
- function isBundleRedundant(bundle, planFeatures, selectedBundles) {
914
- const covered = coverageExcludingSelf(bundle.bundleVersionId, planFeatures, selectedBundles);
915
- return resolveBundleAvailability(bundle, covered) === "covered";
916
- }
917
- function compareSelectable(a, b) {
918
- const orderA = a.sortOrder ?? Number.MAX_SAFE_INTEGER;
919
- const orderB = b.sortOrder ?? Number.MAX_SAFE_INTEGER;
920
- if (orderA !== orderB) return orderA - orderB;
921
- return a.bundleVersionId.localeCompare(b.bundleVersionId);
1492
+ code;
1493
+ reason;
1494
+ params;
1495
+ };
1496
+ function isPersistenceRefusal(err) {
1497
+ if (!(err instanceof Error) || err.name !== PERSISTENCE_REFUSAL) return false;
1498
+ const { code, reason } = err;
1499
+ return typeof code === "string" && (reason === "gone" || reason === "moved");
922
1500
  }
923
- function selectChargeableBundles(planFeatures, selectedBundles) {
924
- const kept = [...selectedBundles].sort(compareSelectable);
925
- for (; ; ) {
926
- const redundantIndex = kept.findIndex(
927
- (bundle) => isBundleRedundant(bundle, planFeatures, kept)
1501
+
1502
+ // src/optional-canonical-models.ts
1503
+ var OPTIONAL_CANONICAL_MODELS = {
1504
+ SuperAdminUser: ["core.superAdminProvisioning"],
1505
+ SuperAdminMfa: ["core.mfa"],
1506
+ AppliedSettings: ["core.appliedSettings"],
1507
+ SettingsChange: ["core.appliedSettings"],
1508
+ MaintenanceWindow: ["core.maintenanceWindows"],
1509
+ SubscriptionNotice: ["tenantBilling.subscriptionNotices"],
1510
+ VersionRetirement: ["tenantBilling.versionRetirements"],
1511
+ SubscriberLedgerEntry: ["entitlement.subscriberLedgerRepository"],
1512
+ PromoCodeHold: ["promo.holdRepository"]
1513
+ };
1514
+ function membersLeftOut(notAdopted = []) {
1515
+ const unknown = notAdopted.filter((model) => !Object.hasOwn(OPTIONAL_CANONICAL_MODELS, model));
1516
+ if (unknown.length > 0) {
1517
+ throw new Error(
1518
+ `notAdopted names ${unknown.join(", ")}, which the persistence bundle cannot leave out. It can leave out ${Object.keys(OPTIONAL_CANONICAL_MODELS).join(", ")}; for any other model it needs, pass your own adapter for the port that model serves.`
928
1519
  );
929
- if (redundantIndex === -1) return kept;
930
- kept.splice(redundantIndex, 1);
931
1520
  }
1521
+ return new Set(
1522
+ notAdopted.flatMap((model) => OPTIONAL_CANONICAL_MODELS[model])
1523
+ );
932
1524
  }
933
1525
 
934
- // src/payment-gateway.types.ts
935
- var PAYMENT_CALLBACK_REJECTED = "PAYMENT_CALLBACK_REJECTED";
936
- var PaymentCallbackRejectedError = class extends Error {
937
- code = PAYMENT_CALLBACK_REJECTED;
938
- constructor(reason) {
939
- super(`Payment callback rejected: ${reason}`);
940
- this.name = "PaymentCallbackRejectedError";
1526
+ // src/concurrency-gate.ts
1527
+ function concurrencyGate(limit) {
1528
+ if (!Number.isInteger(limit) || limit < 1) {
1529
+ throw new RangeError(`A concurrency limit is a whole number of at least 1, not ${limit}.`);
941
1530
  }
942
- };
943
- function isPaymentCallbackRejectedError(err) {
944
- return err instanceof Error && err.code === PAYMENT_CALLBACK_REJECTED;
945
- }
946
-
947
- // src/registration.types.ts
948
- var PENDING_EMAIL_TTL_HOURS = 72;
949
- var PENDING_ONBOARDING_TTL_DAYS = 14;
950
- var PENDING_CHECKOUT_TTL_DAYS = 30;
951
- var OTP_TTL_MINUTES = 10;
952
- var PASSWORD_RESET_TTL_MINUTES = 30;
953
- var OTP_RATE_LIMIT_MAX_SENDS = 3;
954
- var OTP_RATE_LIMIT_WINDOW_MINUTES = 15;
955
- var OTP_VERIFY_MAX_ATTEMPTS = 5;
956
- var REGISTRATION_STEP_BY_STATUS = {
957
- PENDING_EMAIL_VERIFICATION: 2,
958
- EMAIL_VERIFIED: 3,
959
- PLAN_SELECTED: 4,
960
- CHECKOUT_STARTED: 4,
961
- EXPIRED: 1,
962
- DELETED: 1
963
- };
964
- var REGISTRATION_RESUME_TTL_MINUTES = 60;
965
-
966
- // src/plan-mapping.ts
967
- function toPlanRow(row) {
968
- return {
969
- id: row.id,
970
- planKey: row.planKey,
971
- label: row.label,
972
- description: row.description,
973
- icon: row.icon,
974
- sortOrder: row.sortOrder,
975
- createdAt: row.createdAt.toISOString(),
976
- updatedAt: row.updatedAt.toISOString(),
977
- deletedAt: toIsoOrNull(row.deletedAt)
1531
+ let active = 0;
1532
+ const waiting = [];
1533
+ const release = () => {
1534
+ const next = waiting.shift();
1535
+ if (next) next();
1536
+ else active -= 1;
1537
+ };
1538
+ return async (work) => {
1539
+ if (active < limit) active += 1;
1540
+ else await new Promise((admit) => waiting.push(admit));
1541
+ try {
1542
+ return await work();
1543
+ } finally {
1544
+ release();
1545
+ }
978
1546
  };
979
1547
  }
980
- function toPlanVersionRow(row, planKey, fields) {
981
- const mapped = {
982
- id: row.id,
983
- version: row.version,
984
- baseVersionId: row.baseVersionId,
985
- planId: planKey,
986
- features: toFeatureKeys(row.features),
987
- quotas: toQuotaEffects(row.quotas),
988
- monthlyNet: String(row.monthlyNet),
1548
+ var GATES = /* @__PURE__ */ new WeakMap();
1549
+ function concurrencyGateOf(pool, limit) {
1550
+ const known = GATES.get(pool);
1551
+ if (known) {
1552
+ if (known.limit !== limit) {
1553
+ throw new RangeError(
1554
+ `This pool is bounded at ${known.limit} transactions already; a second bound of ${limit} would split it.`
1555
+ );
1556
+ }
1557
+ return known.gate;
1558
+ }
1559
+ const gate = concurrencyGate(limit);
1560
+ GATES.set(pool, { limit, gate });
1561
+ return gate;
1562
+ }
1563
+
1564
+ // src/error-messages.ts
1565
+ var ERROR_MESSAGES_EN = {
1566
+ // ── setup ──
1567
+ SETUP_DISABLED: "{envVar} is not set \u2014 setup is disabled.",
1568
+ INVALID_SETUP_TOKEN: "The setup token is not valid.",
1569
+ SETUP_ALREADY_DONE: "Setup has already been completed.",
1570
+ INVALID_EMAIL: "That is not a valid email address.",
1571
+ EMAIL_EXISTS: "An account with this email address already exists.",
1572
+ // ── auth ──
1573
+ NOT_AUTHENTICATED: "Not authenticated",
1574
+ NO_TENANT_ASSIGNED: "No tenant assigned",
1575
+ TENANT_CONTEXT_MISSING: "No tenant ID found on the request",
1576
+ TENANT_ADMIN_REQUIRED: "This action requires the TENANT_ADMIN role.",
1577
+ BILLING_PERMISSION_REQUIRED: "This part of the billing area requires the billing permission.",
1578
+ SUPER_ADMIN_REQUIRED: "Only the SUPER_ADMIN role is allowed",
1579
+ MFA_NOT_SET_UP: "Run the MFA setup via the CLI first.",
1580
+ MFA_REQUIRED: "TOTP code required in the X-Mfa-Code header.",
1581
+ MFA_FAILED: "Invalid TOTP code.",
1582
+ AUTH_GUARDS_NOT_CONFIGURED: "TenantBillingModule.forRoot.authGuards is not configured.",
1583
+ // ── catalog ──
1584
+ PLAN_HAS_DRAFTS: "Plan '{planKey}' still has {draftCount} open draft version(s). Discard them first (DELETE /admin/catalog/plan-versions/:id) or publish them.",
1585
+ PLAN_HAS_PUBLISHED_VERSIONS: "Plan '{planKey}' cannot be {operation} \u2014 it has {publishedCount} published version(s) ({liveCount} live, {supersededCount} superseded). Existing subscriptions reference those versions (contract protection P1), so the plan record must be preserved.",
1586
+ PLAN_HARD_DELETE_NOT_IMPLEMENTED: "Hard delete is not implemented in the current repository. Implement PlanRepository.hardDelete.",
1587
+ PLAN_VERSION_ALREADY_PUBLISHED: "PlanVersion '{versionId}' is already published; it is not published again or discarded.",
1588
+ PLAN_VERSION_NOT_EDITABLE: "PlanVersion '{versionId}' is not editable. Only drafts and published versions are editable that are latest-in-chain, bind no subscription yet, and whose validFrom lies in the future.",
1589
+ PLAN_VERSION_REGRESSION: "This plan version is regressive (feature removed / quota lowered / price raised). Publishing requires an explicit `forceRegressive: true` (UI confirmation dialog with MFA).",
1590
+ PLAN_VERSION_ZERO_PRICE: "A plan version cannot be published with a price of 0.00 (guard against seed placeholder). For deliberately free special contracts, set allowZeroPrice.",
1591
+ PLAN_VERSION_DISCARD_NOT_IMPLEMENTED: "Discard is not implemented in the current repository. Implement PlanRepository.deletePlanVersionDraft.",
1592
+ PLAN_VERSION_NOT_PUBLISHED: "PlanVersion '{versionId}' is not published and cannot be terminated.",
1593
+ PLAN_VERSION_SUPERSEDED: "PlanVersion '{versionId}' has already been superseded by a newer version and cannot be terminated.",
1594
+ PLAN_VERSION_VALID_FROM_REQUIRED: "validFrom must be set when publishing (on the draft or the publish call)..",
1595
+ PLAN_VERSION_VALID_FROM_INVALID: "validFrom '{validFrom}' is not a day (YYYY-MM-DD)",
1596
+ PLAN_VERSION_VALID_FROM_NOT_AFTER_PREVIOUS: "validFrom ({validFrom}) must be strictly after the validFrom of the previous version ({previousValidFrom}).",
1597
+ PLAN_VERSION_VALID_FROM_NOT_GAPLESS: "The predecessor has validUntil={previousValidUntil} \u2014 the successor must start seamlessly on the next day ({requiredValidFrom}). Received: {received}.",
1598
+ PLAN_VERSION_VALID_UNTIL_INVALID: "validUntil '{validUntil}' is not a day (YYYY-MM-DD)",
1599
+ PLAN_VERSION_VALID_UNTIL_BEFORE_FROM: "validUntil ({validUntil}) must be strictly after validFrom ({validFrom}).",
1600
+ PLAN_TERMINATE_INVALID_DATE: "endsAt is not a valid date.",
1601
+ PLAN_TERMINATE_DATE_NOT_FUTURE: "endsAt ({endsAt}) must lie strictly in the future.",
1602
+ PLAN_TERMINATE_NOT_IMPLEMENTED: "Terminate is not implemented in the current repository. Implement PlanRepository.terminate.",
1603
+ PLAN_TERMINATE_BEFORE_RETIREMENT_MOVES: "Subscriptions still move to version {version} of {planKey}, so it can end on {date} at the earliest.",
1604
+ PLAN_TERMINATE_WHILE_MOVES_OVERDUE: "{count} subscriptions are past their date and still to be moved to version {version} of {planKey}, so it cannot end until they have moved.",
1605
+ PLAN_TERMINATE_WHILE_NOTICES_UNDELIVERED: "{count} subscriptions have not yet been told that they move to version {version} of {planKey}, so it cannot end until they have been.",
1606
+ BUNDLE_VERSION_ALREADY_PUBLISHED: "BundleVersion '{versionId}' is already published; it is not published again or discarded.",
1607
+ BUNDLE_VERSION_NOT_EDITABLE: "BundleVersion '{versionId}' is not editable. Only drafts and published versions are editable that are latest-in-chain, bind no subscription yet, and whose validFrom lies in the future.",
1608
+ BUNDLE_VERSION_NOT_PUBLISHED: "BundleVersion '{bundleVersionId}' is not published and cannot be booked.",
1609
+ BUNDLE_VERSION_SUPERSEDED: "BundleVersion '{bundleVersionId}' has been superseded by a newer version.",
1610
+ BUNDLE_VERSION_NOT_YET_ON_SALE: "BundleVersion '{bundleVersionId}' goes on sale on {validFrom} and cannot be booked before.",
1611
+ BUNDLE_DELETED: "Bundle '{bundleKey}' has been deleted from the catalogue and cannot be booked.",
1612
+ 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).",
1613
+ 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.",
1614
+ BUNDLE_VERSION_NO_PRICE: "This bundle version cannot be published without a price: neither a base price nor any plan override resolves one.",
1615
+ 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.",
1616
+ BUNDLE_VERSION_DISCARD_NOT_IMPLEMENTED: "Discard is not implemented in the current repository. Implement BundleRepository.deleteDraft.",
1617
+ BUNDLE_VERSION_VALID_FROM_REQUIRED: "Published bundle versions must keep a validFrom. Set a new future date, or edit the draft before publishing.",
1618
+ BUNDLE_VERSION_VALID_FROM_INVALID: "validFrom '{validFrom}' is not a day (YYYY-MM-DD)",
1619
+ BUNDLE_VERSION_VALID_FROM_NOT_AFTER_PREVIOUS: "validFrom ({validFrom}) must be strictly after the validFrom of the previous version ({previousValidFrom}).",
1620
+ BUNDLE_VERSION_VALID_FROM_NOT_GAPLESS: "The predecessor has validUntil={previousValidUntil} \u2014 the successor must start seamlessly on the next day ({requiredValidFrom}). Received: {received}.",
1621
+ BUNDLE_VERSION_VALID_FROM_NOT_FUTURE: "validFrom ({validFrom}) must, for a published-but-future bundle version, still lie in the future.",
1622
+ BUNDLE_VERSION_VALID_UNTIL_INVALID: "validUntil '{validUntil}' is not a day (YYYY-MM-DD)",
1623
+ BUNDLE_VERSION_VALID_UNTIL_BEFORE_FROM: "validUntil ({validUntil}) must be strictly after validFrom ({validFrom}).",
1624
+ STRICT_MODE_VIOLATIONS: "The strict-mode check found drift against the discovery snapshot.",
1625
+ PLAN_NOT_FOUND: "Plan '{planId}' not found",
1626
+ PLAN_VERSION_NOT_FOUND: "PlanVersion '{versionId}' not found",
1627
+ BUNDLE_NOT_FOUND: "Bundle '{bundleId}' not found",
1628
+ BUNDLE_VERSION_NOT_FOUND: "BundleVersion '{bundleVersionId}' not found",
1629
+ FEATURE_NOT_FOUND: "Feature '{featureKey}' not found",
1630
+ QUOTA_NOT_FOUND: "Quota '{quotaKey}' not found",
1631
+ PROMOTION_NOT_FOUND: "Promotion '{promotionId}' not found",
1632
+ 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.",
1633
+ MARKETING_PROJECTION_NOT_FOUND: "MarketingProjection '{projectionId}' not found",
1634
+ PLAN_ALREADY_EXISTS: "Plan '{planKey}' already exists",
1635
+ BUNDLE_ALREADY_EXISTS: "Bundle '{bundleKey}' already exists",
1636
+ MARKETING_PROJECTION_ALREADY_EXISTS: "Marketing projection for {targetType}/{targetVersionId}/{locale} already exists \u2014 use PATCH to edit it",
1637
+ PLAN_DRAFT_ALREADY_EXISTS: "Plan '{planKey}' already has a draft version v{draftVersion}; publish or discard it first",
1638
+ BUNDLE_DRAFT_ALREADY_EXISTS: "Bundle '{bundleKey}' already has a draft version v{draftVersion}; publish or discard it first",
1639
+ 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",
1640
+ DISCOVERY_STATUS_TRANSITION_INVALID: "Transition '{from}' \u2192 '{to}' is not allowed",
1641
+ DISCOVERY_NOT_INITIALIZED: "Approval requires a discovery snapshot \u2014 discovery is not initialised (#25)",
1642
+ // ── catalogue import ──
1643
+ 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.",
1644
+ PLAN_CATALOG_INVALID: "The plan catalog was read, and then rejected: {message}",
1645
+ // ── billing ──
1646
+ BUNDLE_ALREADY_SUBSCRIBED: "Subscription '{subscriptionId}' has already actively booked this bundle.",
1647
+ BUNDLE_INCOMPATIBLE_WITH_PLAN: "This bundle cannot be booked on the {planKey} plan. The plans that can book it: {allowedPlanKeys}.",
1648
+ BUNDLE_NOT_SELF_SERVICE: "Bundle '{bundleKey}' is only activated via a special contract. Please contact the contract manager.",
1649
+ 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.",
1650
+ BUNDLE_NOT_PRICED_FOR_THIS_PLAN: "This bundle has no price for the {planKey} plan in this billing cycle, so it cannot be booked from here.",
1651
+ BUNDLE_CANNOT_RUN_ON_UPCOMING_PLAN: "This bundle cannot run on the {planKey} plan, which the subscription moves to with effect from {from}.",
1652
+ BUNDLE_CANNOT_RUN_ON_UPCOMING_CYCLE: "A yearly bundle cannot run beside the monthly billing the subscription moves to with effect from {from}.",
1653
+ SUBSCRIPTION_BUNDLE_ALREADY_CANCELLED: "SubscriptionBundle '{subscriptionBundleId}' is already cancelled.",
1654
+ SUBSCRIPTION_BUNDLE_NOT_CANCELLED: "SubscriptionBundle '{subscriptionBundleId}' is not cancelled.",
1655
+ SUBSCRIPTION_BUNDLE_CANCELLATION_EFFECTIVE: "Cancellation already in effect \u2014 book the bundle again.",
1656
+ SUBSCRIPTION_NOT_FOUND: "No subscription for tenant {tenantId}",
1657
+ SUBSCRIPTION_TENANT_MISMATCH: "Subscription does not belong to the tenant",
1658
+ SUBSCRIPTION_BUNDLE_NOT_FOUND: "SubscriptionBundle '{subscriptionBundleId}' not found",
1659
+ TENANT_NOT_FOUND: "Tenant {slug} not found",
1660
+ NO_ACTIVE_PLAN_VERSION: "No version of plan {planId} is on sale as of {asOf}.",
1661
+ BOUND_PLAN_VERSION_UNREADABLE: 'The plan version this subscription is bound to ({planVersionId}) cannot be read for plan "{planKey}", so no change can be quoted.',
1662
+ PLAN_NOT_IN_CATALOG: 'Plan "{planKey}" is not in the catalog',
1663
+ PLAN_NOT_SELF_SERVICE: "{planName} is only activated via a special contract. Please contact the contract manager.",
1664
+ PLAN_NOT_SOLD_IN_CYCLE: "{planName} has no price for this billing rhythm and cannot be booked in it.",
1665
+ PLAN_CHANGE_BLOCKED: "Plan change during onboarding is blocked.",
1666
+ SUBSCRIPTION_CHANGED: "This subscription changed while the request was being decided. Reload it.",
1667
+ NO_SUBSCRIPTION: "This tenant has no subscription to cancel.",
1668
+ CANCELLATION_TERMS_CHANGED: "The effective date changed since it was shown. Confirm the new one.",
1669
+ VERSION_OFFER_CHANGED: "The offer changed since it was shown. Look at the current one before switching.",
1670
+ PLAN_CHANGE_QUOTE_CHANGED: "This plan changed since it was shown. Look at it again before changing to it.",
1671
+ PLAN_CHANGE_VERSION_NOT_NAMED: "A plan change has to name the version its preview showed.",
1672
+ VERSION_SWITCH_AFTER_CANCELLATION: "This subscription ends before this version would take effect, so it cannot be switched to.",
1673
+ VERSION_ENDS_BEFORE_SWITCH: "This version stops being sold before it would take effect, so it cannot be switched to.",
1674
+ RETIREMENT_TERMS_NOT_CONFIRMED: "Retiring a version for running subscriptions needs a clause in your terms. Set tenantBilling.orderlyRetirement.termsConfirmed in config/saas.yaml once they carry it.",
1675
+ RETIREMENT_VERSION_ON_SALE: "Version {version} of {planKey} is still on sale. End its sale first, so nobody books it after the announcement.",
1676
+ RETIREMENT_REPLACEMENT_NOT_ON_SALE: "Version {version} of {planKey} is not on sale, so subscriptions cannot continue on it.",
1677
+ RETIREMENT_REPLACEMENT_NOT_SOLD_IN_RHYTHM: "{count} of these subscriptions are billed in a rhythm version {version} of {planKey} has no price for, so they cannot continue on it.",
1678
+ RETIREMENT_REPLACEMENT_CANNOT_CARRY_BUNDLES: "{count} of these subscriptions hold a bundle that cannot run on version {version} of {planKey}, so they cannot continue on it.",
1679
+ RETIREMENT_REPLACEMENT_IS_RETIRED: "A version cannot be its own replacement.",
1680
+ RETIREMENT_NOTHING_AFFECTED: "No running subscription is on version {version} of {planKey}, so there is nobody to tell.",
1681
+ RETIREMENT_WITHIN_TWELVE_MONTHS: "{count} of these subscriptions were reached by a retirement within the last twelve months. A subscription is reached at most once a year.",
1682
+ RETIREMENT_PREVIEW_CHANGED: "The subscriptions this retirement reaches changed since they were shown. Look at them again before announcing it.",
1683
+ RETIREMENT_SWITCH_NOT_PENDING: "No retirement of your version is waiting for its date, so there is nothing to switch to.",
1684
+ RETIREMENT_SWITCH_IN_TRIAL: "The switch opens when your trial ends.",
1685
+ RETIREMENT_SWITCH_BUNDLE_CANNOT_FOLLOW: "{bundleName} cannot run on {planName} and runs until {until} at the earliest. Once it is cancelled, the switch can be made from that day.",
1686
+ RETIREMENT_SWITCH_NOT_OPEN: "This subscription cannot switch now: a change is scheduled, it has ended, or its plan is held for a special contract.",
1687
+ RETIREMENT_SWITCH_CHANGED: "The retirement changed since it was shown. Look at it again before switching.",
1688
+ SUBSCRIPTION_ENDED: "This subscription has ended. Its plan can no longer be changed.",
1689
+ PLAN_LOCKED: "Active {planName} special contract \u2014 please contact the contract manager to change plans.",
1690
+ QUOTA_OVER_TARGET: "Current usage {used} exceeds the target limit {targetMax} ({quotaKey}) in the {planName} plan. Please reduce usage first.",
1691
+ FEATURE_LOST: "Switching means losing access to one feature. Existing data is retained and never deleted \u2014 upgrading again unlocks it.",
1692
+ FEATURES_LOST: "Switching means losing access to {count} features. Existing data is retained and never deleted \u2014 upgrading again unlocks it.",
1693
+ NO_CHANGE: "Target plan and billing cycle already match the current state.",
1694
+ 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.",
1695
+ 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.",
1696
+ BUNDLE_BOOKING_OUTLASTS_TARGET_CYCLE: "{bundleName} is billed yearly and runs until {until} at the earliest, which a monthly plan cannot carry. Once it is cancelled, a change that takes effect on or after that day goes through \u2014 or keep the yearly cycle.",
1697
+ BUNDLE_BOOKING_DOES_NOT_FIT_TARGET_PLAN: "{bundleName} cannot run on {planName} and runs until {until} at the earliest. Once it is cancelled, a change that takes effect on or after that day goes through \u2014 or choose another plan.",
1698
+ REDUNDANT_FEATURES: "The plan or another booked bundle already includes {count} of the features in this bundle \u2014 booking it pays for them twice.",
1699
+ MINIMUM_TERM_BINDS: "The minimum term extends beyond the end of the period \u2014 the cancellation only takes effect when the minimum term ends.",
1700
+ BUNDLE_FEATURE_DEPENDENCY_UNSATISFIED: "The bundle requires [{features}] \u2014 present neither in the plan nor in the active bundles.",
1701
+ ONBOARDING_CREATE_FAILED: "The account could not be created. Please try again.",
1702
+ BUNDLE_PREVIEW_ARGUMENT_AMBIGUOUS: "Exactly one of bundleVersionId (add preview) or subscriptionBundleId (cancel preview) must be given.",
1703
+ SUBSCRIPTION_PK_MISSING: "The adapter returned a subscription usage record without an id. Pass the subscription primary key through (see SubscriptionUsageRecord.id).",
1704
+ LIMIT_EXCEEDED: "The limit for {dimension} has been reached: {used} of {max}.",
1705
+ QUOTA_DIMENSION_UNKNOWN: 'Unknown quota dimension "{dimension}".',
1706
+ // ── promo ──
1707
+ PROMO_CODE_NOT_FOUND: "Code not found",
1708
+ PROMO_CODE_ALREADY_EXISTS: "The code exists, or a deleted code carries it; a deleted code keeps its name.",
1709
+ PROMO_CODE_HAS_REDEMPTIONS: "The code already has redemptions, or checkouts holding one \u2014 it cannot be soft-deleted. Pause it instead.",
1710
+ PROMO_CODE_NOT_REDEEMABLE: "Code cannot be redeemed: {reason}",
1711
+ PROMO_CODE_FORMAT_INVALID: 'The code may only contain upper-case letters, digits, "-" and "_" (4\u201332 characters).',
1712
+ PROMO_PERCENT_OUT_OF_RANGE: "The percentage must be between 0 and 100.",
1713
+ PROMO_AMOUNT_NOT_POSITIVE: "The amount must be positive.",
1714
+ PROMO_ONE_OFF_WITH_DURATION: "A one-off discount must not set a duration.",
1715
+ PROMO_DURATION_INVALID: "Invalid duration (at most 24 months or billing periods).",
1716
+ PROMO_VALIDITY_WINDOW_INVALID: "Invalid validity window.",
1717
+ PROMO_PLAN_NOT_DISCOUNTABLE: "{plan}-Plan cannot be discounted.",
1718
+ PROMO_MIN_AMOUNT_NOT_POSITIVE: "The minimum plan gross amount must be positive.",
1719
+ PROMO_WOULD_PRODUCE_ZERO_INVOICE: "For absolute amounts the discount must stay below the lowest applicable plan price, or allowZeroInvoice must be enabled.",
1720
+ PROMO_MAX_REDEMPTIONS_LOWERED: "maxRedemptions cannot be lowered.",
1721
+ // ── contract ──
1722
+ CHECKOUT_OFFER_LINE_ITEMS_REQUIRED: "A checkout offer can yield only one contract, and only once its line items are frozen.",
1723
+ CHECKOUT_OFFER_PLAN_LINE_ITEM_REQUIRED: "A checkout offer requires a frozen plan line item.",
1724
+ CHECKOUT_OFFER_BUNDLE_LINE_ITEMS_REQUIRED: "Every selected bundle version requires a frozen bundle line item.",
1725
+ CHECKOUT_OFFER_BUNDLE_VERSION_NOT_BOOKABLE: "At least one bundle version from the checkout offer is no longer bookable.",
1726
+ CHECKOUT_OFFER_FEATURE_DEPENDENCY_UNSATISFIED: "The selected plan does not cover all feature dependencies: [{missingRequires}] are missing from the plan + selected bundles.",
1727
+ CHECKOUT_OFFER_PLAN_NOT_OFFERED: "Plan '{planKey}' is not offered with a {billingCycle} price at the moment.",
1728
+ CHECKOUT_OFFER_BUNDLE_NOT_OFFERED: "Bundle version '{bundleVersionId}' cannot be added to this offer ({reason}).",
1729
+ CHECKOUT_OFFER_PROMO_CODE_NOT_ACCEPTED: "The promo code cannot be applied to this offer ({reason}).",
1730
+ CHECKOUT_OFFER_PRICE_NOT_CURRENT: "The prices of checkout offer '{offerId}' no longer match the catalogue; create a new offer.",
1731
+ SUBSCRIPTION_CONTRACT_LINE_ITEMS_REQUIRED: "A subscription contract requires at least one line item.",
1732
+ SUBSCRIPTION_CONTRACT_PLAN_LINE_ITEM_REQUIRED: "A subscription contract requires exactly one plan base item.",
1733
+ SUBSCRIPTION_CONTRACT_INVALID_DATE: "{field} must be a valid date.",
1734
+ SUBSCRIPTION_CONTRACT_INVALID_WINDOW: "effectiveUntil must be after effectiveFrom.",
1735
+ SUBSCRIPTION_CONTRACT_LINE_ITEM_TAX_MISMATCH: "A line item's taxAmount must be exactly priceGross minus priceNet.",
1736
+ 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.",
1737
+ SUBSCRIPTION_CONTRACT_TERMINATION_BEFORE_START: "effectiveUntil must be after the effectiveFrom of the contract.",
1738
+ SUBSCRIPTION_CONTRACT_LINE_ITEM_CURRENCY_MISMATCH: "A line item must be booked in the currency its contract was priced in.",
1739
+ 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}.",
1740
+ SUBSCRIPTION_CONTRACT_DISCOUNT_NEGATIVE: "A subscription contract states a discount of {amount} at {field}; a discount takes money off and is never negative.",
1741
+ CHECKOUT_OFFER_NOT_FOUND: "CheckoutOffer '{offerId}' not found",
1742
+ CHECKOUT_OFFER_EXPIRED: "Checkout offer '{offerId}' has expired and cannot be {action}",
1743
+ CHECKOUT_OFFER_ALREADY_CONSUMED: "Checkout offer '{offerId}' has already been consumed and cannot be {action}",
1744
+ CHECKOUT_OFFER_NOT_CONSUMED: "CheckoutOffer '{offerId}' must be consumed before the contract is created",
1745
+ CHECKOUT_OFFER_CHANGED: "Checkout offer '{offerId}' changed while it was being concluded. Load it again.",
1746
+ SUBSCRIPTION_CONTRACT_NOT_FOUND: "SubscriptionContract '{contractId}' not found",
1747
+ NO_ACTIVE_SUBSCRIPTION_CONTRACT: "No active subscription contract for tenant {tenantId}",
1748
+ SUBSCRIPTION_CONTRACT_ALREADY_CLOSED: "SubscriptionContract '{contractId}' is already closed",
1749
+ SUBSCRIPTION_CONTRACT_CHANGED: "The contracts of tenant '{tenantId}' changed while a successor was being written, or one begins after the moment it would take effect, so it would run beside another. Nothing was written.",
1750
+ // ── subscriber ──
1751
+ SUBSCRIBER_REQUIRED: "Tenant '{tenantId}' has no subscriber. Nothing is agreed or charged without the party to it: create the tenant's subscriber first.",
1752
+ SUBSCRIBER_ALREADY_EXISTS: "Tenant '{tenantId}' already has a subscriber.",
1753
+ SUBSCRIBER_NOT_FOUND: "Subscriber '{subscriberId}' not found",
1754
+ SUBSCRIBER_LEGAL_NAME_REQUIRED: "A subscriber needs its legal name.",
1755
+ SUBSCRIBER_DETAIL_INVALID: "The subscriber's {field} is not valid.",
1756
+ SUBSCRIBER_IDENTITY_NOT_A_CONTACT: "{field} is part of the subscriber's legal identity and changes only as a correction, with a reason.",
1757
+ SUBSCRIBER_CORRECTION_REASON_REQUIRED: "A correction of the legal identity needs a reason.",
1758
+ SUBSCRIBER_CORRECTION_ACTOR_REQUIRED: "A correction of the legal identity has to say who makes it.",
1759
+ SUBSCRIBER_CORRECTION_CHANGES_NOTHING: "The correction changes nothing: every value it names is already recorded.",
1760
+ 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.",
1761
+ // ── registration ──
1762
+ PENDING_REGISTRATION_NOT_FOUND: "This registration could not be found. It may have already been completed or discarded.",
1763
+ PENDING_REGISTRATION_EXPIRED: "This registration has expired. Please start again.",
1764
+ INVALID_REGISTRATION_STATE: "This step is not available at the current stage of the registration.",
1765
+ OTP_INVALID: "That code is not correct. Please check it and try again.",
1766
+ OTP_EXPIRED: "That code has expired. Please request a new one.",
1767
+ OTP_LOCKED: "Too many incorrect attempts. Please request a new code.",
1768
+ RATE_LIMITED: "Too many attempts. Please try again in {retryAfterSeconds} seconds.",
1769
+ RESUME_TOKEN_INVALID: "This resume link is no longer valid. Please request a new one.",
1770
+ RESUME_NOT_CONFIGURED: "Resuming a registration is not available.",
1771
+ CONFIGURATOR_NOT_CONFIGURED: "The configurator is not available.",
1772
+ CONFIG_NOT_SAVED: "The configuration could not be saved.",
1773
+ PLAN_NOT_AVAILABLE: "This plan is not available.",
1774
+ PLAN_NOT_SELECTED: "Please select a plan first.",
1775
+ MODEL_NOT_AVAILABLE: "This option is not available.",
1776
+ // ── payments ──
1777
+ PAYMENTS_NOT_CONFIGURED: "Payment methods cannot be set up here yet.",
1778
+ PAYMENT_GATEWAY_ACCOUNT_UNKNOWN: "No payment gateway account is configured under '{account}'.",
1779
+ PAYMENT_CALLBACK_REJECTED: "The payment callback could not be verified.",
1780
+ PAYMENT_RETURN_URL_NOT_ALLOWED: "The {field} leads to a site this installation does not return to.",
1781
+ PAYMENT_GATEWAY_FAILED: "The payment provider did not answer as expected. Please try again in a few minutes.",
1782
+ // ── entitlement (code lives in upsell.types.ts) ──
1783
+ [FEATURE_NOT_LICENSED]: "Feature {featureKeys} is not included in the current plan.",
1784
+ // ── settings ──
1785
+ SETTINGS_CHANGE_NOT_FOUND: "No recorded settings change has this id.",
1786
+ // ── maintenance ──
1787
+ MAINTENANCE: "The application is under maintenance.",
1788
+ MAINTENANCE_WINDOW_ALREADY_OPEN: "A maintenance window is already open. Move or cancel it before announcing another.",
1789
+ MAINTENANCE_WINDOW_NOT_OPEN: "No open maintenance window has this id.",
1790
+ MAINTENANCE_WINDOW_LOCKED: "This maintenance window is locked. Its start cannot move, and unlocking is what ends it.",
1791
+ MAINTENANCE_WINDOW_END_NOT_AFTER_START: "The end of a maintenance window has to be after its start.",
1792
+ MAINTENANCE_WINDOW_END_IN_PAST: "The end of a maintenance window cannot be in the past.",
1793
+ MAINTENANCE_TIME_INVALID: "The {field} has to be a date and time with its zone, such as 2026-10-02T22:00+02:00.",
1794
+ MAINTENANCE_MESSAGE_TOO_LONG: "The message may be at most {max} characters long."
1795
+ };
1796
+ function formatErrorMessage(template, params = {}) {
1797
+ return template.replace(/\{(\w+)\}/g, (match, name) => {
1798
+ const value = params[name];
1799
+ return value === void 0 || value === null ? match : String(value);
1800
+ });
1801
+ }
1802
+ function resolveErrorMessage(body, overrides = {}, defaults = ERROR_MESSAGES_EN) {
1803
+ const code = typeof body.code === "string" ? body.code : void 0;
1804
+ const template = (code && (overrides[code] ?? defaults[code])) ?? void 0;
1805
+ if (!template) {
1806
+ return typeof body.message === "string" ? body.message : code ?? "";
1807
+ }
1808
+ const { params, ...rest } = body;
1809
+ return formatErrorMessage(template, { ...rest, ...params });
1810
+ }
1811
+
1812
+ // src/refusal.ts
1813
+ function refusal(code, reason, params) {
1814
+ return new PersistenceRefusal(
1815
+ code,
1816
+ reason,
1817
+ formatErrorMessage(ERROR_MESSAGES_EN[code], params),
1818
+ params
1819
+ );
1820
+ }
1821
+
1822
+ // src/catalog-version-refusals.ts
1823
+ var CASES = {
1824
+ PlanVersion: {
1825
+ gone: { code: CATALOG_ERROR_CODES.PLAN_VERSION_NOT_FOUND, param: "versionId" },
1826
+ published: { code: CATALOG_ERROR_CODES.PLAN_VERSION_ALREADY_PUBLISHED, param: "versionId" },
1827
+ draft: { code: CATALOG_ERROR_CODES.PLAN_DRAFT_ALREADY_EXISTS, param: "planKey" }
1828
+ },
1829
+ BundleVersion: {
1830
+ gone: { code: CATALOG_ERROR_CODES.BUNDLE_VERSION_NOT_FOUND, param: "bundleVersionId" },
1831
+ published: {
1832
+ code: CATALOG_ERROR_CODES.BUNDLE_VERSION_ALREADY_PUBLISHED,
1833
+ param: "versionId"
1834
+ },
1835
+ draft: { code: CATALOG_ERROR_CODES.BUNDLE_DRAFT_ALREADY_EXISTS, param: "bundleKey" }
1836
+ }
1837
+ };
1838
+ function catalogVersionGone(kind, versionId) {
1839
+ const { code, param } = CASES[kind].gone;
1840
+ return new PersistenceRefusal(code, "gone", `${kind} '${versionId}' not found.`, {
1841
+ [param]: versionId
1842
+ });
1843
+ }
1844
+ function catalogVersionAlreadyPublished(kind, versionId) {
1845
+ const { code, param } = CASES[kind].published;
1846
+ return new PersistenceRefusal(
1847
+ code,
1848
+ "moved",
1849
+ `${kind} '${versionId}' is already published; it is not published again or discarded.`,
1850
+ { [param]: versionId }
1851
+ );
1852
+ }
1853
+ function catalogDraftExists(kind, key, draftVersion) {
1854
+ const { code, param } = CASES[kind].draft;
1855
+ return refusal(code, "moved", { [param]: key, draftVersion });
1856
+ }
1857
+
1858
+ // src/catalog-key-refusals.ts
1859
+ function planKeyTaken(planKey) {
1860
+ return refusal(CATALOG_ERROR_CODES.PLAN_ALREADY_EXISTS, "moved", { planKey });
1861
+ }
1862
+ function bundleKeyTaken(bundleKey) {
1863
+ return refusal(CATALOG_ERROR_CODES.BUNDLE_ALREADY_EXISTS, "moved", { bundleKey });
1864
+ }
1865
+ function marketingProjectionTaken(target) {
1866
+ return refusal(CATALOG_ERROR_CODES.MARKETING_PROJECTION_ALREADY_EXISTS, "moved", {
1867
+ targetType: target.targetType,
1868
+ targetVersionId: target.targetVersionId,
1869
+ locale: target.locale
1870
+ });
1871
+ }
1872
+
1873
+ // src/decimal-string.ts
1874
+ function toDecimalString(value) {
1875
+ if (!Number.isFinite(value)) {
1876
+ throw new RangeError(`${String(value)} is not an amount a numeric column can hold.`);
1877
+ }
1878
+ return String(value);
1879
+ }
1880
+
1881
+ // src/promo-code-refusals.ts
1882
+ function promoCodeTaken(code) {
1883
+ return new PersistenceRefusal(
1884
+ PROMO_ERROR_CODES.PROMO_CODE_ALREADY_EXISTS,
1885
+ "moved",
1886
+ `Promo code '${code}' is taken.`,
1887
+ { promoCode: code }
1888
+ );
1889
+ }
1890
+
1891
+ // src/subscription-refusals.ts
1892
+ function subscriptionGone(tenantId) {
1893
+ return refusal(BILLING_ERROR_CODES.SUBSCRIPTION_NOT_FOUND, "gone", { tenantId });
1894
+ }
1895
+ function noActivePlanVersion(planId, asOf) {
1896
+ return refusal(BILLING_ERROR_CODES.NO_ACTIVE_PLAN_VERSION, "gone", {
1897
+ planId,
1898
+ asOf: asOf.toISOString().slice(0, 10)
1899
+ });
1900
+ }
1901
+ function planNotInCatalog(planKey) {
1902
+ return refusal(BILLING_ERROR_CODES.PLAN_NOT_IN_CATALOG, "gone", { planKey });
1903
+ }
1904
+ function subscriptionBundleGone(subscriptionBundleId) {
1905
+ return refusal(BILLING_ERROR_CODES.SUBSCRIPTION_BUNDLE_NOT_FOUND, "gone", {
1906
+ subscriptionBundleId
1907
+ });
1908
+ }
1909
+ function subscriptionBundleAlreadyCancelled(subscriptionBundleId) {
1910
+ return refusal(BILLING_ERROR_CODES.SUBSCRIPTION_BUNDLE_ALREADY_CANCELLED, "moved", {
1911
+ subscriptionBundleId
1912
+ });
1913
+ }
1914
+ function subscriptionContractGone(contractId) {
1915
+ return refusal(CONTRACT_ERROR_CODES.SUBSCRIPTION_CONTRACT_NOT_FOUND, "gone", { contractId });
1916
+ }
1917
+
1918
+ // src/feature-requires.ts
1919
+ function buildFeatureRequiresIndex(features) {
1920
+ const index = /* @__PURE__ */ new Map();
1921
+ for (const feature of features) {
1922
+ const requires = (feature.requires ?? []).filter((key) => key !== feature.featureKey);
1923
+ if (requires.length > 0) index.set(feature.featureKey, requires);
1924
+ }
1925
+ return index;
1926
+ }
1927
+ function collectUnsatisfiedRequires(selected, index) {
1928
+ const own = new Set(selected);
1929
+ const unsatisfied = /* @__PURE__ */ new Set();
1930
+ for (const featureKey of selected) {
1931
+ for (const required of index.get(featureKey) ?? []) {
1932
+ if (!own.has(required)) unsatisfied.add(required);
1933
+ }
1934
+ }
1935
+ return [...unsatisfied].sort((a, b) => a.localeCompare(b));
1936
+ }
1937
+ function missingRequiresFor(bundle, coveredFeatures) {
1938
+ const missing = (bundle.requiresFeatures ?? []).filter((key) => !coveredFeatures.has(key));
1939
+ return [...new Set(missing)].sort((a, b) => a.localeCompare(b));
1940
+ }
1941
+ function resolveBundleAvailability(bundle, coveredFeatures) {
1942
+ if (bundle.features.length > 0 && bundle.features.every((f) => coveredFeatures.has(f))) {
1943
+ return "covered";
1944
+ }
1945
+ if (missingRequiresFor(bundle, coveredFeatures).length > 0) return "missing-requires";
1946
+ return "bookable";
1947
+ }
1948
+ function coverageExcludingSelf(selfVersionId, planFeatures, selectedBundles) {
1949
+ const covered = new Set(planFeatures);
1950
+ for (const bundle of selectedBundles) {
1951
+ if (bundle.bundleVersionId === selfVersionId) continue;
1952
+ for (const feature of bundle.features) covered.add(feature);
1953
+ }
1954
+ return covered;
1955
+ }
1956
+ function isBundleRedundant(bundle, planFeatures, selectedBundles) {
1957
+ const covered = coverageExcludingSelf(bundle.bundleVersionId, planFeatures, selectedBundles);
1958
+ return resolveBundleAvailability(bundle, covered) === "covered";
1959
+ }
1960
+ function compareSelectable(a, b) {
1961
+ const orderA = a.sortOrder ?? Number.MAX_SAFE_INTEGER;
1962
+ const orderB = b.sortOrder ?? Number.MAX_SAFE_INTEGER;
1963
+ if (orderA !== orderB) return orderA - orderB;
1964
+ return a.bundleVersionId.localeCompare(b.bundleVersionId);
1965
+ }
1966
+ function selectChargeableBundles(planFeatures, selectedBundles) {
1967
+ const kept = [...selectedBundles].sort(compareSelectable);
1968
+ for (; ; ) {
1969
+ const redundantIndex = kept.findIndex(
1970
+ (bundle) => isBundleRedundant(bundle, planFeatures, kept)
1971
+ );
1972
+ if (redundantIndex === -1) return kept;
1973
+ kept.splice(redundantIndex, 1);
1974
+ }
1975
+ }
1976
+
1977
+ // src/payment-gateway.types.ts
1978
+ var PAYMENT_CALLBACK_REJECTED = "PAYMENT_CALLBACK_REJECTED";
1979
+ var PaymentCallbackRejectedError = class extends Error {
1980
+ code = PAYMENT_CALLBACK_REJECTED;
1981
+ constructor(reason) {
1982
+ super(`Payment callback rejected: ${reason}`);
1983
+ this.name = "PaymentCallbackRejectedError";
1984
+ }
1985
+ };
1986
+ function isPaymentCallbackRejectedError(err) {
1987
+ return err instanceof Error && err.code === PAYMENT_CALLBACK_REJECTED;
1988
+ }
1989
+
1990
+ // src/registration.types.ts
1991
+ var PENDING_EMAIL_TTL_HOURS = 72;
1992
+ var PENDING_ONBOARDING_TTL_DAYS = 14;
1993
+ var PENDING_CHECKOUT_TTL_DAYS = 30;
1994
+ var OTP_TTL_MINUTES = 10;
1995
+ var PASSWORD_RESET_TTL_MINUTES = 30;
1996
+ var OTP_RATE_LIMIT_MAX_SENDS = 3;
1997
+ var OTP_RATE_LIMIT_WINDOW_MINUTES = 15;
1998
+ var OTP_VERIFY_MAX_ATTEMPTS = 5;
1999
+ var REGISTRATION_STEP_BY_STATUS = {
2000
+ PENDING_EMAIL_VERIFICATION: 2,
2001
+ EMAIL_VERIFIED: 3,
2002
+ PLAN_SELECTED: 4,
2003
+ CHECKOUT_STARTED: 4,
2004
+ EXPIRED: 1,
2005
+ DELETED: 1
2006
+ };
2007
+ var REGISTRATION_RESUME_TTL_MINUTES = 60;
2008
+
2009
+ // src/plan-mapping.ts
2010
+ function toPlanRow(row) {
2011
+ return {
2012
+ id: row.id,
2013
+ planKey: row.planKey,
2014
+ label: row.label,
2015
+ description: row.description,
2016
+ icon: row.icon,
2017
+ sortOrder: row.sortOrder,
2018
+ createdAt: row.createdAt.toISOString(),
2019
+ updatedAt: row.updatedAt.toISOString(),
2020
+ deletedAt: toIsoOrNull(row.deletedAt)
2021
+ };
2022
+ }
2023
+ function toPlanVersionRow(row, planKey) {
2024
+ return {
2025
+ id: row.id,
2026
+ version: row.version,
2027
+ baseVersionId: row.baseVersionId,
2028
+ planId: planKey,
2029
+ features: toFeatureKeys(row.features),
2030
+ quotas: toQuotaEffects(row.quotas),
2031
+ monthlyNet: String(row.monthlyNet),
989
2032
  yearlyNet: String(row.yearlyNet),
990
2033
  marketed: row.marketed,
991
2034
  publishedAt: toIsoOrNull(row.publishedAt),
@@ -993,17 +2036,14 @@ function toPlanVersionRow(row, planKey, fields) {
993
2036
  publishedChanges: Array.isArray(row.publishedChanges) ? row.publishedChanges : null,
994
2037
  changeNote: row.changeNote,
995
2038
  nonRegressive: row.nonRegressive,
996
- validFrom: fields.validityWindows ? toIsoOrNull(row.validFrom) : null,
997
- validUntil: fields.validityWindows ? toIsoOrNull(row.validUntil) : null,
2039
+ validFrom: toIsoOrNull(row.validFrom),
2040
+ validUntil: toIsoOrNull(row.validUntil),
2041
+ endsAt: toIsoOrNull(row.endsAt),
998
2042
  createdByUserId: row.createdByUserId,
999
2043
  publishedByUserId: row.publishedByUserId,
1000
2044
  createdAt: row.createdAt.toISOString(),
1001
2045
  updatedAt: row.updatedAt.toISOString()
1002
2046
  };
1003
- if (fields.endsAt) {
1004
- mapped.endsAt = toIsoOrNull(row.endsAt);
1005
- }
1006
- return mapped;
1007
2047
  }
1008
2048
  function toIsoOrNull(value) {
1009
2049
  return value ? value.toISOString() : null;
@@ -1102,17 +2142,6 @@ function textOrNull(value) {
1102
2142
  return trimmed === "" ? null : trimmed;
1103
2143
  }
1104
2144
 
1105
- // src/closed-value.ts
1106
- function oneOf(allowed, value, source) {
1107
- const match = allowed.find((entry) => entry === value);
1108
- if (match === void 0) {
1109
- throw new Error(
1110
- `${source.table} row '${source.id}' holds ${source.column} '${value}', which is none of ${allowed.join(", ")}.`
1111
- );
1112
- }
1113
- return match;
1114
- }
1115
-
1116
2145
  // src/subscriber-ledger-mapping.ts
1117
2146
  var ORIGINS = {
1118
2147
  activation: true,
@@ -1127,10 +2156,10 @@ var SOURCES = {
1127
2156
  bundle: true,
1128
2157
  discount: true
1129
2158
  };
1130
- var TABLE = "subscriber_ledger_entries";
2159
+ var TABLE2 = "subscriber_ledger_entries";
1131
2160
  var CENT_TOLERANCE = 1e-6;
1132
2161
  function toSubscriberChargeRecord(row) {
1133
- const where = (column) => ({ table: TABLE, column, id: row.id });
2162
+ const where = (column) => ({ table: TABLE2, column, id: row.id });
1134
2163
  return {
1135
2164
  id: row.id,
1136
2165
  subscriberId: row.subscriberId,
@@ -1150,8 +2179,8 @@ function toSubscriberChargeRecord(row) {
1150
2179
  };
1151
2180
  }
1152
2181
  function subscriberChargeColumns(charge) {
1153
- const cents = charge.amountNet * 100;
1154
- if (!Number.isFinite(cents) || Math.abs(cents - Math.round(cents)) > CENT_TOLERANCE) {
2182
+ const cents2 = charge.amountNet * 100;
2183
+ if (!Number.isFinite(cents2) || Math.abs(cents2 - Math.round(cents2)) > CENT_TOLERANCE) {
1155
2184
  throw new Error(
1156
2185
  `A charge of ${charge.amountNet} is not a whole number of cents; round it once where it is derived, before it is written.`
1157
2186
  );
@@ -1294,7 +2323,7 @@ function toIdentityValues(value) {
1294
2323
  // src/subscriber-payment-method-mapping.ts
1295
2324
  var PAYMENT_METHOD_TYPES = ["card", "sepa_debit"];
1296
2325
  var STATUSES = ["ACTIVE", "REPLACED"];
1297
- var TABLE2 = "subscriber_payment_methods";
2326
+ var TABLE3 = "subscriber_payment_methods";
1298
2327
  function toSubscriberPaymentMethodRecord(row) {
1299
2328
  return {
1300
2329
  id: row.id,
@@ -1303,7 +2332,7 @@ function toSubscriberPaymentMethodRecord(row) {
1303
2332
  provider: row.provider,
1304
2333
  customerRef: row.customerRef,
1305
2334
  paymentMethodRef: row.paymentMethodRef,
1306
- type: oneOf(PAYMENT_METHOD_TYPES, row.type, { table: TABLE2, column: "type", id: row.id }),
2335
+ type: oneOf(PAYMENT_METHOD_TYPES, row.type, { table: TABLE3, column: "type", id: row.id }),
1307
2336
  brand: row.brand,
1308
2337
  last4: row.last4,
1309
2338
  expiryMonth: row.expiryMonth,
@@ -1311,7 +2340,7 @@ function toSubscriberPaymentMethodRecord(row) {
1311
2340
  country: row.country,
1312
2341
  bankCode: row.bankCode,
1313
2342
  mandateReference: row.mandateReference,
1314
- status: oneOf(STATUSES, row.status, { table: TABLE2, column: "status", id: row.id }),
2343
+ status: oneOf(STATUSES, row.status, { table: TABLE3, column: "status", id: row.id }),
1315
2344
  confirmedAt: row.confirmedAt,
1316
2345
  replacedAt: row.replacedAt,
1317
2346
  createdAt: row.createdAt
@@ -1492,6 +2521,52 @@ var ACTIVE_SUBSCRIPTION_CONTRACT_STATUSES = [
1492
2521
  "scheduled"
1493
2522
  ];
1494
2523
 
2524
+ // src/custom-limits.ts
2525
+ var CUSTOM_LIMITS_KEYS = ["quotas", "features"];
2526
+ var NOT_AN_OBJECT = "(the value is not an object)";
2527
+ function readCustomLimits(value) {
2528
+ if (value === null || value === void 0) return { limits: null, unread: [] };
2529
+ if (typeof value !== "object" || Array.isArray(value)) {
2530
+ return { limits: null, unread: [NOT_AN_OBJECT] };
2531
+ }
2532
+ const limits = {};
2533
+ const unread = [];
2534
+ for (const [key, entry] of Object.entries(value)) {
2535
+ if (key === "quotas" && isRecord(entry)) {
2536
+ limits.quotas = {};
2537
+ for (const [quota, amount] of Object.entries(entry)) {
2538
+ const value2 = readQuotaValue(amount);
2539
+ if (value2 === null) unread.push(`quotas.${quota}`);
2540
+ else limits.quotas[quota] = value2;
2541
+ }
2542
+ } else if (key === "features" && isFeatureList(entry)) {
2543
+ limits.features = [...entry];
2544
+ } else {
2545
+ unread.push(key);
2546
+ }
2547
+ }
2548
+ return { limits, unread };
2549
+ }
2550
+ function isRecord(value) {
2551
+ return value !== null && typeof value === "object" && !Array.isArray(value);
2552
+ }
2553
+ function isFeatureList(value) {
2554
+ return Array.isArray(value) && value.every((entry) => typeof entry === "string");
2555
+ }
2556
+ function customLimitsReader(report) {
2557
+ const reported = /* @__PURE__ */ new Set();
2558
+ return (subscriptionId, stored) => {
2559
+ const { limits, unread } = readCustomLimits(stored);
2560
+ if (unread.length > 0 && !reported.has(subscriptionId)) {
2561
+ reported.add(subscriptionId);
2562
+ report(
2563
+ `Subscription ${subscriptionId}: customLimits carries ${unread.join(", ")}, which the platform does not read, so these limits are not applied. It reads { quotas: { <quotaKey>: number }, features: string[] } (CustomLimits in @saasicat/core).`
2564
+ );
2565
+ }
2566
+ return limits;
2567
+ };
2568
+ }
2569
+
1495
2570
  // src/version-editability.ts
1496
2571
  function isVersionEditable(v, now = /* @__PURE__ */ new Date()) {
1497
2572
  if (v.publishedAt === null) {
@@ -1530,217 +2605,6 @@ function keepOneRecommended(plans, inRequestedLocale) {
1530
2605
  return winner;
1531
2606
  }
1532
2607
 
1533
- // src/error-messages.ts
1534
- var ERROR_MESSAGES_EN = {
1535
- // ── setup ──
1536
- SETUP_DISABLED: "{envVar} is not set \u2014 setup is disabled.",
1537
- INVALID_SETUP_TOKEN: "The setup token is not valid.",
1538
- SETUP_ALREADY_DONE: "Setup has already been completed.",
1539
- INVALID_EMAIL: "That is not a valid email address.",
1540
- EMAIL_EXISTS: "An account with this email address already exists.",
1541
- // ── auth ──
1542
- NOT_AUTHENTICATED: "Not authenticated",
1543
- NO_TENANT_ASSIGNED: "No tenant assigned",
1544
- TENANT_CONTEXT_MISSING: "No tenant ID found on the request",
1545
- TENANT_ADMIN_REQUIRED: "This action requires the TENANT_ADMIN role.",
1546
- BILLING_PERMISSION_REQUIRED: "This part of the billing area requires the billing permission.",
1547
- SUPER_ADMIN_REQUIRED: "Only the SUPER_ADMIN role is allowed",
1548
- MFA_NOT_SET_UP: "Run the MFA setup via the CLI first.",
1549
- MFA_REQUIRED: "TOTP code required in the X-Mfa-Code header.",
1550
- MFA_FAILED: "Invalid TOTP code.",
1551
- AUTH_GUARDS_NOT_CONFIGURED: "TenantBillingModule.forRoot.authGuards is not configured.",
1552
- // ── catalog ──
1553
- PLAN_HAS_DRAFTS: "Plan '{planKey}' still has {draftCount} open draft version(s). Discard them first (DELETE /admin/catalog/plan-versions/:id) or publish them.",
1554
- PLAN_HAS_PUBLISHED_VERSIONS: "Plan '{planKey}' cannot be {operation} \u2014 it has {publishedCount} published version(s) ({liveCount} live, {supersededCount} superseded). Existing subscriptions reference those versions (contract protection P1), so the plan record must be preserved.",
1555
- PLAN_HARD_DELETE_NOT_IMPLEMENTED: "Hard delete is not implemented in the current repository. Implement PlanRepository.hardDelete.",
1556
- PLAN_VERSION_ALREADY_PUBLISHED: "PlanVersion '{versionId}' is already published and cannot be discarded.",
1557
- PLAN_VERSION_NOT_EDITABLE: "PlanVersion '{versionId}' is not editable. Only drafts and published versions are editable that are latest-in-chain, bind no subscription yet, and whose validFrom lies in the future.",
1558
- PLAN_VERSION_REGRESSION: "This plan version is regressive (feature removed / quota lowered / price raised). Publishing requires an explicit `forceRegressive: true` (UI confirmation dialog with MFA).",
1559
- PLAN_VERSION_ZERO_PRICE: "A plan version cannot be published with a price of 0.00 (guard against seed placeholder). For deliberately free special contracts, set allowZeroPrice.",
1560
- PLAN_VERSION_DISCARD_NOT_IMPLEMENTED: "Discard is not implemented in the current repository. Implement PlanRepository.deletePlanVersionDraft.",
1561
- PLAN_VERSION_NOT_PUBLISHED: "PlanVersion '{versionId}' is not published and cannot be terminated.",
1562
- PLAN_VERSION_SUPERSEDED: "PlanVersion '{versionId}' has already been superseded by a newer version and cannot be terminated.",
1563
- PLAN_VERSION_VALID_FROM_REQUIRED: "validFrom must be set when publishing (on the draft or the publish call)..",
1564
- PLAN_VERSION_VALID_FROM_INVALID: "validFrom '{validFrom}' is not a valid date",
1565
- PLAN_VERSION_VALID_FROM_NOT_AFTER_PREVIOUS: "validFrom ({validFrom}) must be strictly after the validFrom of the previous version ({previousValidFrom}).",
1566
- PLAN_VERSION_VALID_FROM_NOT_GAPLESS: "The predecessor has validUntil={previousValidUntil} \u2014 the successor must start seamlessly on the next day ({requiredValidFrom}). Received: {received}.",
1567
- PLAN_VERSION_VALID_UNTIL_INVALID: "validUntil '{validUntil}' is not a valid date",
1568
- PLAN_VERSION_VALID_UNTIL_BEFORE_FROM: "validUntil ({validUntil}) must be strictly after validFrom ({validFrom}).",
1569
- PLAN_TERMINATE_INVALID_DATE: "endsAt is not a valid date.",
1570
- PLAN_TERMINATE_DATE_NOT_FUTURE: "endsAt ({endsAt}) must lie strictly in the future.",
1571
- PLAN_TERMINATE_NOT_IMPLEMENTED: "Terminate is not implemented in the current repository. Implement PlanRepository.terminate.",
1572
- BUNDLE_VERSION_ALREADY_PUBLISHED: "BundleVersion '{versionId}' is already published and cannot be discarded.",
1573
- BUNDLE_VERSION_NOT_EDITABLE: "BundleVersion '{versionId}' is not editable. Only drafts and published versions are editable that are latest-in-chain, bind no subscription yet, and whose validFrom lies in the future.",
1574
- BUNDLE_VERSION_NOT_PUBLISHED: "BundleVersion '{bundleVersionId}' is not published and cannot be booked.",
1575
- BUNDLE_VERSION_SUPERSEDED: "BundleVersion '{bundleVersionId}' has been superseded by a newer version.",
1576
- 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).",
1577
- 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.",
1578
- BUNDLE_VERSION_NO_PRICE: "This bundle version cannot be published without a price: neither a base price nor any plan override resolves one.",
1579
- 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.",
1580
- BUNDLE_VERSION_DISCARD_NOT_IMPLEMENTED: "Discard is not implemented in the current repository. Implement BundleRepository.deleteDraft.",
1581
- BUNDLE_VERSION_VALID_FROM_REQUIRED: "Published bundle versions must keep a validFrom. Set a new future date, or edit the draft before publishing.",
1582
- BUNDLE_VERSION_VALID_FROM_INVALID: "validFrom '{validFrom}' is not a valid date",
1583
- BUNDLE_VERSION_VALID_FROM_NOT_AFTER_PREVIOUS: "validFrom ({validFrom}) must be strictly after the validFrom of the previous version ({previousValidFrom}).",
1584
- BUNDLE_VERSION_VALID_FROM_NOT_GAPLESS: "The predecessor has validUntil={previousValidUntil} \u2014 the successor must start seamlessly on the next day ({requiredValidFrom}). Received: {received}.",
1585
- BUNDLE_VERSION_VALID_FROM_NOT_FUTURE: "validFrom ({validFrom}) must, for a published-but-future bundle version, still lie in the future.",
1586
- BUNDLE_VERSION_VALID_UNTIL_INVALID: "validUntil '{validUntil}' is not a valid date",
1587
- BUNDLE_VERSION_VALID_UNTIL_BEFORE_FROM: "validUntil ({validUntil}) must be strictly after validFrom ({validFrom}).",
1588
- STRICT_MODE_VIOLATIONS: "The strict-mode check found drift against the discovery snapshot.",
1589
- PLAN_NOT_FOUND: "Plan '{planId}' not found",
1590
- PLAN_VERSION_NOT_FOUND: "PlanVersion '{versionId}' not found",
1591
- BUNDLE_NOT_FOUND: "Bundle '{bundleId}' not found",
1592
- BUNDLE_VERSION_NOT_FOUND: "BundleVersion '{bundleVersionId}' not found",
1593
- FEATURE_NOT_FOUND: "Feature '{featureKey}' not found",
1594
- QUOTA_NOT_FOUND: "Quota '{quotaKey}' not found",
1595
- PROMOTION_NOT_FOUND: "Promotion '{promotionId}' not found",
1596
- 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.",
1597
- MARKETING_PROJECTION_NOT_FOUND: "MarketingProjection '{projectionId}' not found",
1598
- PLAN_ALREADY_EXISTS: "Plan '{planKey}' already exists",
1599
- BUNDLE_ALREADY_EXISTS: "Bundle '{bundleKey}' already exists",
1600
- MARKETING_PROJECTION_ALREADY_EXISTS: "Marketing projection for {targetType}/{targetVersionId}/{locale} already exists \u2014 use PATCH to edit it",
1601
- PLAN_DRAFT_ALREADY_EXISTS: "Plan '{planKey}' already has a draft version v{draftVersion}; publish or discard it first",
1602
- BUNDLE_DRAFT_ALREADY_EXISTS: "Bundle '{bundleKey}' already has a draft version v{draftVersion}; publish or discard it first",
1603
- 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",
1604
- DISCOVERY_STATUS_TRANSITION_INVALID: "Transition '{from}' \u2192 '{to}' is not allowed",
1605
- DISCOVERY_NOT_INITIALIZED: "Approval requires a discovery snapshot \u2014 discovery is not initialised (#25)",
1606
- // ── catalogue import ──
1607
- 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.",
1608
- PLAN_CATALOG_INVALID: "The plan catalog was read, and then rejected: {message}",
1609
- // ── billing ──
1610
- BUNDLE_ALREADY_SUBSCRIBED: "Subscription '{subscriptionId}' has already actively booked this bundle.",
1611
- BUNDLE_INCOMPATIBLE_WITH_PLAN: "BundleVersion '{bundleVersionId}' is not compatible with plan '{planKey}'. Allowed: [{allowedPlanKeys}].",
1612
- BUNDLE_NOT_SELF_SERVICE: "Bundle '{bundleKey}' is only activated via a special contract. Please contact the contract manager.",
1613
- 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.",
1614
- BUNDLE_NOT_PRICED_FOR_THIS_PLAN: "This bundle has no {billingCycle} price for the {planKey} plan, so it cannot be booked from here.",
1615
- SUBSCRIPTION_BUNDLE_ALREADY_CANCELLED: "SubscriptionBundle '{subscriptionBundleId}' is already cancelled.",
1616
- SUBSCRIPTION_BUNDLE_NOT_CANCELLED: "SubscriptionBundle '{subscriptionBundleId}' is not cancelled.",
1617
- SUBSCRIPTION_BUNDLE_CANCELLATION_EFFECTIVE: "Cancellation already in effect \u2014 book the bundle again.",
1618
- SUBSCRIPTION_NOT_FOUND: "No subscription for tenant {tenantId}",
1619
- SUBSCRIPTION_TENANT_MISMATCH: "Subscription does not belong to the tenant",
1620
- SUBSCRIPTION_BUNDLE_NOT_FOUND: "SubscriptionBundle '{subscriptionBundleId}' not found",
1621
- TENANT_NOT_FOUND: "Tenant {slug} not found",
1622
- 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.",
1623
- PLAN_NOT_IN_CATALOG: 'Plan "{planKey}" is not in the catalog',
1624
- PLAN_NOT_SELF_SERVICE: "{planName} is only activated via a special contract. Please contact the contract manager.",
1625
- PLAN_NOT_SOLD_IN_CYCLE: "{planName} has no price for this billing rhythm and cannot be booked in it.",
1626
- PLAN_CHANGE_BLOCKED: "Plan change during onboarding is blocked.",
1627
- SUBSCRIPTION_CHANGED: "This subscription changed while the request was being decided. Reload it.",
1628
- NO_SUBSCRIPTION: "This tenant has no subscription to cancel.",
1629
- CANCELLATION_TERMS_CHANGED: "The effective date changed since it was shown. Confirm the new one.",
1630
- SUBSCRIPTION_ENDED: "This subscription has ended. Its plan can no longer be changed.",
1631
- PLAN_LOCKED: "Active {planName} special contract \u2014 please contact the contract manager to change plans.",
1632
- QUOTA_OVER_TARGET: "Current usage {used} exceeds the target limit {targetMax} ({quotaKey}) in the {planName} plan. Please reduce usage first.",
1633
- FEATURE_LOST: "Switching means losing access to one feature. Existing data is retained and never deleted \u2014 upgrading again unlocks it.",
1634
- FEATURES_LOST: "Switching means losing access to {count} features. Existing data is retained and never deleted \u2014 upgrading again unlocks it.",
1635
- NO_CHANGE: "Target plan and billing cycle already match the current state.",
1636
- 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.",
1637
- 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.",
1638
- 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.",
1639
- REDUNDANT_FEATURES: "The plan or another booked bundle already includes {count} of the features in this bundle \u2014 booking it pays for them twice.",
1640
- MINIMUM_TERM_BINDS: "The minimum term extends beyond the end of the period \u2014 the cancellation only takes effect when the minimum term ends.",
1641
- BUNDLE_FEATURE_DEPENDENCY_UNSATISFIED: "The bundle requires [{features}] \u2014 present neither in the plan nor in the active bundles.",
1642
- NO_PENDING_PLAN_VERSION: "There is no pending plan version awaiting confirmation.",
1643
- ONBOARDING_CREATE_FAILED: "The account could not be created. Please try again.",
1644
- BUNDLE_PREVIEW_ARGUMENT_AMBIGUOUS: "Exactly one of bundleVersionId (add preview) or subscriptionBundleId (cancel preview) must be given.",
1645
- SUBSCRIPTION_PK_MISSING: "The adapter returned a subscription usage record without an id. Pass the subscription primary key through (see SubscriptionUsageRecord.id).",
1646
- LIMIT_EXCEEDED: "The limit for {dimension} has been reached: {used} of {max}.",
1647
- QUOTA_DIMENSION_UNKNOWN: 'Unknown quota dimension "{dimension}".',
1648
- // ── promo ──
1649
- PROMO_CODE_NOT_FOUND: "Code not found",
1650
- PROMO_CODE_ALREADY_EXISTS: "The code already exists.",
1651
- PROMO_CODE_HAS_REDEMPTIONS: "The code already has redemptions, or checkouts holding one \u2014 it cannot be soft-deleted. Pause it instead.",
1652
- PROMO_CODE_NOT_REDEEMABLE: "Code cannot be redeemed: {reason}",
1653
- PROMO_CODE_FORMAT_INVALID: 'The code may only contain upper-case letters, digits, "-" and "_" (4\u201332 characters).',
1654
- PROMO_PERCENT_OUT_OF_RANGE: "The percentage must be between 0 and 100.",
1655
- PROMO_AMOUNT_NOT_POSITIVE: "The amount must be positive.",
1656
- PROMO_ONE_OFF_WITH_DURATION: "A one-off discount must not set a duration.",
1657
- PROMO_DURATION_INVALID: "Invalid duration (at most 24 months or billing periods).",
1658
- PROMO_VALIDITY_WINDOW_INVALID: "Invalid validity window.",
1659
- PROMO_PLAN_NOT_DISCOUNTABLE: "{plan}-Plan cannot be discounted.",
1660
- PROMO_MIN_AMOUNT_NOT_POSITIVE: "The minimum plan gross amount must be positive.",
1661
- PROMO_WOULD_PRODUCE_ZERO_INVOICE: "For absolute amounts the discount must stay below the lowest applicable plan price, or allowZeroInvoice must be enabled.",
1662
- PROMO_MAX_REDEMPTIONS_LOWERED: "maxRedemptions cannot be lowered.",
1663
- // ── contract ──
1664
- CHECKOUT_OFFER_LINE_ITEMS_REQUIRED: "A checkout offer can yield only one contract, and only once its line items are frozen.",
1665
- CHECKOUT_OFFER_PLAN_LINE_ITEM_REQUIRED: "A checkout offer requires a frozen plan line item.",
1666
- CHECKOUT_OFFER_BUNDLE_LINE_ITEMS_REQUIRED: "Every selected bundle version requires a frozen bundle line item.",
1667
- CHECKOUT_OFFER_BUNDLE_VERSION_NOT_BOOKABLE: "At least one bundle version from the checkout offer is no longer bookable.",
1668
- CHECKOUT_OFFER_FEATURE_DEPENDENCY_UNSATISFIED: "The selected plan does not cover all feature dependencies: [{missingRequires}] are missing from the plan + selected bundles.",
1669
- CHECKOUT_OFFER_PLAN_NOT_OFFERED: "Plan '{planKey}' is not offered with a {billingCycle} price at the moment.",
1670
- CHECKOUT_OFFER_BUNDLE_NOT_OFFERED: "Bundle version '{bundleVersionId}' cannot be added to this offer ({reason}).",
1671
- CHECKOUT_OFFER_PROMO_CODE_NOT_ACCEPTED: "The promo code cannot be applied to this offer ({reason}).",
1672
- CHECKOUT_OFFER_PRICE_NOT_CURRENT: "The prices of checkout offer '{offerId}' no longer match the catalogue; create a new offer.",
1673
- SUBSCRIPTION_CONTRACT_LINE_ITEMS_REQUIRED: "A subscription contract requires at least one line item.",
1674
- SUBSCRIPTION_CONTRACT_PLAN_LINE_ITEM_REQUIRED: "A subscription contract requires exactly one plan base item.",
1675
- SUBSCRIPTION_CONTRACT_INVALID_DATE: "{field} must be a valid date.",
1676
- SUBSCRIPTION_CONTRACT_INVALID_WINDOW: "effectiveUntil must be after effectiveFrom.",
1677
- SUBSCRIPTION_CONTRACT_LINE_ITEM_TAX_MISMATCH: "A line item's taxAmount must be exactly priceGross minus priceNet.",
1678
- 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.",
1679
- SUBSCRIPTION_CONTRACT_TERMINATION_BEFORE_START: "effectiveUntil must be after the effectiveFrom of the contract.",
1680
- SUBSCRIPTION_CONTRACT_LINE_ITEM_CURRENCY_MISMATCH: "A line item must be booked in the currency its contract was priced in.",
1681
- 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}.",
1682
- SUBSCRIPTION_CONTRACT_DISCOUNT_NEGATIVE: "A subscription contract states a discount of {amount} at {field}; a discount takes money off and is never negative.",
1683
- CHECKOUT_OFFER_NOT_FOUND: "CheckoutOffer '{offerId}' not found",
1684
- CHECKOUT_OFFER_EXPIRED: "Checkout offer '{offerId}' has expired and cannot be {action}",
1685
- CHECKOUT_OFFER_ALREADY_CONSUMED: "Checkout offer '{offerId}' has already been consumed and cannot be {action}",
1686
- CHECKOUT_OFFER_NOT_CONSUMED: "CheckoutOffer '{offerId}' must be consumed before the contract is created",
1687
- CHECKOUT_OFFER_CHANGED: "Checkout offer '{offerId}' changed while it was being concluded. Load it again.",
1688
- SUBSCRIPTION_CONTRACT_NOT_FOUND: "SubscriptionContract '{contractId}' not found",
1689
- NO_ACTIVE_SUBSCRIPTION_CONTRACT: "No active subscription contract for tenant {tenantId}",
1690
- SUBSCRIPTION_CONTRACT_ALREADY_CLOSED: "SubscriptionContract '{contractId}' is already closed",
1691
- // ── subscriber ──
1692
- SUBSCRIBER_REQUIRED: "Tenant '{tenantId}' has no subscriber. Nothing is agreed or charged without the party to it: create the tenant's subscriber first.",
1693
- SUBSCRIBER_ALREADY_EXISTS: "Tenant '{tenantId}' already has a subscriber.",
1694
- SUBSCRIBER_NOT_FOUND: "Subscriber '{subscriberId}' not found",
1695
- SUBSCRIBER_LEGAL_NAME_REQUIRED: "A subscriber needs its legal name.",
1696
- SUBSCRIBER_DETAIL_INVALID: "The subscriber's {field} is not valid.",
1697
- SUBSCRIBER_IDENTITY_NOT_A_CONTACT: "{field} is part of the subscriber's legal identity and changes only as a correction, with a reason.",
1698
- SUBSCRIBER_CORRECTION_REASON_REQUIRED: "A correction of the legal identity needs a reason.",
1699
- SUBSCRIBER_CORRECTION_ACTOR_REQUIRED: "A correction of the legal identity has to say who makes it.",
1700
- SUBSCRIBER_CORRECTION_CHANGES_NOTHING: "The correction changes nothing: every value it names is already recorded.",
1701
- 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.",
1702
- // ── registration ──
1703
- PENDING_REGISTRATION_NOT_FOUND: "This registration could not be found. It may have already been completed or discarded.",
1704
- PENDING_REGISTRATION_EXPIRED: "This registration has expired. Please start again.",
1705
- INVALID_REGISTRATION_STATE: "This step is not available at the current stage of the registration.",
1706
- OTP_INVALID: "That code is not correct. Please check it and try again.",
1707
- OTP_EXPIRED: "That code has expired. Please request a new one.",
1708
- OTP_LOCKED: "Too many incorrect attempts. Please request a new code.",
1709
- RATE_LIMITED: "Too many attempts. Please try again in {retryAfterSeconds} seconds.",
1710
- RESUME_TOKEN_INVALID: "This resume link is no longer valid. Please request a new one.",
1711
- RESUME_NOT_CONFIGURED: "Resuming a registration is not available.",
1712
- CONFIGURATOR_NOT_CONFIGURED: "The configurator is not available.",
1713
- CONFIG_NOT_SAVED: "The configuration could not be saved.",
1714
- PLAN_NOT_AVAILABLE: "This plan is not available.",
1715
- PLAN_NOT_SELECTED: "Please select a plan first.",
1716
- MODEL_NOT_AVAILABLE: "This option is not available.",
1717
- // ── payments ──
1718
- PAYMENTS_NOT_CONFIGURED: "Payment methods cannot be set up here yet.",
1719
- PAYMENT_GATEWAY_ACCOUNT_UNKNOWN: "No payment gateway account is configured under '{account}'.",
1720
- PAYMENT_CALLBACK_REJECTED: "The payment callback could not be verified.",
1721
- PAYMENT_RETURN_URL_NOT_ALLOWED: "The {field} leads to a site this installation does not return to.",
1722
- PAYMENT_GATEWAY_FAILED: "The payment provider did not answer as expected. Please try again in a few minutes.",
1723
- // ── entitlement (code lives in upsell.types.ts) ──
1724
- [FEATURE_NOT_LICENSED]: "Feature {featureKeys} is not included in the current plan.",
1725
- // ── settings ──
1726
- SETTINGS_CHANGE_NOT_FOUND: "No recorded settings change has this id."
1727
- };
1728
- function formatErrorMessage(template, params = {}) {
1729
- return template.replace(/\{(\w+)\}/g, (match, name) => {
1730
- const value = params[name];
1731
- return value === void 0 || value === null ? match : String(value);
1732
- });
1733
- }
1734
- function resolveErrorMessage(body, overrides = {}, defaults = ERROR_MESSAGES_EN) {
1735
- const code = typeof body.code === "string" ? body.code : void 0;
1736
- const template = (code && (overrides[code] ?? defaults[code])) ?? void 0;
1737
- if (!template) {
1738
- return typeof body.message === "string" ? body.message : code ?? "";
1739
- }
1740
- const { params, ...rest } = body;
1741
- return formatErrorMessage(template, { ...rest, ...params });
1742
- }
1743
-
1744
2608
  // src/error-messages-de.ts
1745
2609
  var ERROR_MESSAGES_DE = {
1746
2610
  // ── setup ──
@@ -1764,7 +2628,7 @@ var ERROR_MESSAGES_DE = {
1764
2628
  PLAN_HAS_DRAFTS: "Plan '{planKey}' hat noch {draftCount} offene Entwurfsversion(en). Verwirf sie zuerst (DELETE /admin/catalog/plan-versions/:id) oder ver\xF6ffentliche sie.",
1765
2629
  PLAN_HAS_PUBLISHED_VERSIONS: "Plan '{planKey}' kann nicht {operation} werden \u2014 er hat {publishedCount} ver\xF6ffentlichte Version(en) ({liveCount} live, {supersededCount} abgel\xF6st). Bestehende Abonnements verweisen auf diese Versionen (Vertragsschutz P1), deshalb muss der Plan-Datensatz erhalten bleiben.",
1766
2630
  PLAN_HARD_DELETE_NOT_IMPLEMENTED: "Hard Delete ist im aktuellen Repository nicht implementiert. Implementiere PlanRepository.hardDelete.",
1767
- PLAN_VERSION_ALREADY_PUBLISHED: "PlanVersion '{versionId}' ist bereits ver\xF6ffentlicht und kann nicht verworfen werden.",
2631
+ PLAN_VERSION_ALREADY_PUBLISHED: "PlanVersion '{versionId}' ist bereits ver\xF6ffentlicht und wird weder erneut ver\xF6ffentlicht noch verworfen.",
1768
2632
  PLAN_VERSION_NOT_EDITABLE: "PlanVersion '{versionId}' ist nicht bearbeitbar. Bearbeitbar sind nur Entw\xFCrfe sowie ver\xF6ffentlichte Versionen, die letzte in der Kette sind, noch kein Abonnement binden und deren validFrom in der Zukunft liegt.",
1769
2633
  PLAN_VERSION_REGRESSION: "Diese Planversion 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).",
1770
2634
  PLAN_VERSION_ZERO_PRICE: "Eine Planversion l\xE4sst sich nicht mit einem Preis von 0,00 ver\xF6ffentlichen (Schutz vor Seed-Platzhaltern). F\xFCr bewusst kostenlose Sondervertr\xE4ge setze allowZeroPrice.",
@@ -1772,29 +2636,34 @@ var ERROR_MESSAGES_DE = {
1772
2636
  PLAN_VERSION_NOT_PUBLISHED: "Die PlanVersion '{versionId}' ist nicht ver\xF6ffentlicht und kann nicht beendet werden.",
1773
2637
  PLAN_VERSION_SUPERSEDED: "Die PlanVersion '{versionId}' wurde bereits von einer Nachfolgeversion abgel\xF6st und kann nicht beendet werden.",
1774
2638
  PLAN_VERSION_VALID_FROM_REQUIRED: "validFrom muss beim Ver\xF6ffentlichen gesetzt sein (am Entwurf oder im Publish-Aufruf)..",
1775
- PLAN_VERSION_VALID_FROM_INVALID: "validFrom '{validFrom}' ist kein g\xFCltiges Datum",
2639
+ PLAN_VERSION_VALID_FROM_INVALID: "validFrom '{validFrom}' ist kein Tag (JJJJ-MM-TT)",
1776
2640
  PLAN_VERSION_VALID_FROM_NOT_AFTER_PREVIOUS: "validFrom ({validFrom}) muss strikt nach dem validFrom der Vorg\xE4ngerversion ({previousValidFrom}) liegen.",
1777
2641
  PLAN_VERSION_VALID_FROM_NOT_GAPLESS: "Der Vorg\xE4nger hat validUntil={previousValidUntil} \u2014 der Nachfolger muss l\xFCckenlos am Folgetag ({requiredValidFrom}) beginnen. Erhalten: {received}.",
1778
- PLAN_VERSION_VALID_UNTIL_INVALID: "validUntil '{validUntil}' ist kein g\xFCltiges Datum",
2642
+ PLAN_VERSION_VALID_UNTIL_INVALID: "validUntil '{validUntil}' ist kein Tag (JJJJ-MM-TT)",
1779
2643
  PLAN_VERSION_VALID_UNTIL_BEFORE_FROM: "validUntil ({validUntil}) muss strikt nach validFrom ({validFrom}) liegen.",
1780
2644
  PLAN_TERMINATE_INVALID_DATE: "endsAt ist kein g\xFCltiges Datum.",
1781
2645
  PLAN_TERMINATE_DATE_NOT_FUTURE: "endsAt ({endsAt}) muss strikt in der Zukunft liegen.",
1782
2646
  PLAN_TERMINATE_NOT_IMPLEMENTED: "Das Beenden ist im aktuellen Repository nicht implementiert. Implementiere PlanRepository.terminate.",
1783
- BUNDLE_VERSION_ALREADY_PUBLISHED: "BundleVersion '{versionId}' ist bereits ver\xF6ffentlicht und kann nicht verworfen werden.",
2647
+ PLAN_TERMINATE_BEFORE_RETIREMENT_MOVES: "Abonnements ziehen noch auf Version {version} von {planKey} um. Sie kann fr\xFChestens am {date} enden.",
2648
+ PLAN_TERMINATE_WHILE_MOVES_OVERDUE: "{count} Abonnements sind \xFCber ihr Datum und ziehen noch auf Version {version} von {planKey} um. Sie kann erst enden, wenn sie umgezogen sind.",
2649
+ PLAN_TERMINATE_WHILE_NOTICES_UNDELIVERED: "{count} Abonnements wissen noch nicht, dass sie auf Version {version} von {planKey} umziehen. Sie kann erst enden, wenn sie es erfahren haben.",
2650
+ BUNDLE_VERSION_ALREADY_PUBLISHED: "BundleVersion '{versionId}' ist bereits ver\xF6ffentlicht und wird weder erneut ver\xF6ffentlicht noch verworfen.",
1784
2651
  BUNDLE_VERSION_NOT_EDITABLE: "BundleVersion '{versionId}' ist nicht bearbeitbar. Bearbeitbar sind nur Entw\xFCrfe sowie ver\xF6ffentlichte Versionen, die letzte in der Kette sind, noch kein Abonnement binden und deren validFrom in der Zukunft liegt.",
1785
2652
  BUNDLE_VERSION_NOT_PUBLISHED: "BundleVersion '{bundleVersionId}' ist nicht ver\xF6ffentlicht und l\xE4sst sich nicht buchen.",
1786
2653
  BUNDLE_VERSION_SUPERSEDED: "BundleVersion '{bundleVersionId}' wurde durch eine neuere Version abgel\xF6st.",
2654
+ BUNDLE_VERSION_NOT_YET_ON_SALE: "BundleVersion '{bundleVersionId}' ist ab {validFrom} im Verkauf und kann vorher nicht gebucht werden.",
2655
+ BUNDLE_DELETED: "Bundle '{bundleKey}' wurde aus dem Katalog gel\xF6scht und l\xE4sst sich nicht buchen.",
1787
2656
  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).",
1788
2657
  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.",
1789
2658
  BUNDLE_VERSION_NO_PRICE: "Diese Bundle-Version kann ohne Preis nicht ver\xF6ffentlicht werden: weder ein Grundpreis noch eine Plan-\xDCberschreibung ergibt einen.",
1790
2659
  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.",
1791
2660
  BUNDLE_VERSION_DISCARD_NOT_IMPLEMENTED: "Das Verwerfen ist im aktuellen Repository nicht implementiert. Implementiere BundleRepository.deleteDraft.",
1792
2661
  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.",
1793
- BUNDLE_VERSION_VALID_FROM_INVALID: "validFrom '{validFrom}' ist kein g\xFCltiges Datum",
2662
+ BUNDLE_VERSION_VALID_FROM_INVALID: "validFrom '{validFrom}' ist kein Tag (JJJJ-MM-TT)",
1794
2663
  BUNDLE_VERSION_VALID_FROM_NOT_AFTER_PREVIOUS: "validFrom ({validFrom}) muss strikt nach dem validFrom der Vorg\xE4ngerversion ({previousValidFrom}) liegen.",
1795
2664
  BUNDLE_VERSION_VALID_FROM_NOT_GAPLESS: "Der Vorg\xE4nger hat validUntil={previousValidUntil} \u2014 der Nachfolger muss l\xFCckenlos am Folgetag ({requiredValidFrom}) beginnen. Erhalten: {received}.",
1796
2665
  BUNDLE_VERSION_VALID_FROM_NOT_FUTURE: "validFrom ({validFrom}) muss bei einer ver\xF6ffentlichten, aber zuk\xFCnftigen Bundle-Version weiterhin in der Zukunft liegen.",
1797
- BUNDLE_VERSION_VALID_UNTIL_INVALID: "validUntil '{validUntil}' ist kein g\xFCltiges Datum",
2666
+ BUNDLE_VERSION_VALID_UNTIL_INVALID: "validUntil '{validUntil}' ist kein Tag (JJJJ-MM-TT)",
1798
2667
  BUNDLE_VERSION_VALID_UNTIL_BEFORE_FROM: "validUntil ({validUntil}) muss strikt nach validFrom ({validFrom}) liegen.",
1799
2668
  STRICT_MODE_VIOLATIONS: "Die Strict-Mode-Pr\xFCfung hat eine Abweichung gegen\xFCber dem Discovery-Snapshot gefunden.",
1800
2669
  PLAN_NOT_FOUND: "Plan '{planId}' nicht gefunden",
@@ -1819,10 +2688,12 @@ var ERROR_MESSAGES_DE = {
1819
2688
  PLAN_CATALOG_INVALID: "Der Plankatalog wurde gelesen und dann abgelehnt: {message}",
1820
2689
  // ── billing ──
1821
2690
  BUNDLE_ALREADY_SUBSCRIBED: "Abonnement '{subscriptionId}' hat dieses Bundle bereits aktiv gebucht.",
1822
- BUNDLE_INCOMPATIBLE_WITH_PLAN: "BundleVersion '{bundleVersionId}' passt nicht zum Plan '{planKey}'. Erlaubt: [{allowedPlanKeys}].",
2691
+ BUNDLE_INCOMPATIBLE_WITH_PLAN: "Dieses Bundle kann im Plan {planKey} nicht gebucht werden. Buchbar ist es in: {allowedPlanKeys}.",
1823
2692
  BUNDLE_NOT_SELF_SERVICE: "Bundle '{bundleKey}' wird nur \xFCber einen Sondervertrag freigeschaltet. Bitte wende dich an die Vertragsverwaltung.",
1824
2693
  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.",
1825
- 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.",
2694
+ BUNDLE_NOT_PRICED_FOR_THIS_PLAN: "F\xFCr dieses Bundle ist im Plan {planKey} in diesem Abrechnungsrhythmus kein Preis hinterlegt, es kann hier deshalb nicht gebucht werden.",
2695
+ BUNDLE_CANNOT_RUN_ON_UPCOMING_PLAN: "Dieses Bundle kann nicht im Plan {planKey} laufen, in den das Abonnement mit Wirkung ab {from} wechselt.",
2696
+ BUNDLE_CANNOT_RUN_ON_UPCOMING_CYCLE: "Ein j\xE4hrlich abgerechnetes Bundle kann nicht neben der monatlichen Abrechnung laufen, auf die das Abonnement mit Wirkung ab {from} wechselt.",
1826
2697
  SUBSCRIPTION_BUNDLE_ALREADY_CANCELLED: "SubscriptionBundle '{subscriptionBundleId}' ist bereits gek\xFCndigt.",
1827
2698
  SUBSCRIPTION_BUNDLE_NOT_CANCELLED: "SubscriptionBundle '{subscriptionBundleId}' ist nicht gek\xFCndigt.",
1828
2699
  SUBSCRIPTION_BUNDLE_CANCELLATION_EFFECTIVE: "Die K\xFCndigung ist bereits wirksam \u2014 buche das Bundle einfach neu.",
@@ -1830,7 +2701,8 @@ var ERROR_MESSAGES_DE = {
1830
2701
  SUBSCRIPTION_TENANT_MISMATCH: "Das Abonnement geh\xF6rt nicht zu diesem Mandanten",
1831
2702
  SUBSCRIPTION_BUNDLE_NOT_FOUND: "SubscriptionBundle '{subscriptionBundleId}' nicht gefunden",
1832
2703
  TENANT_NOT_FOUND: "Mandant {slug} nicht gefunden",
1833
- 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.",
2704
+ NO_ACTIVE_PLAN_VERSION: "Zum {asOf} ist keine Version des Plans {planId} im Verkauf.",
2705
+ BOUND_PLAN_VERSION_UNREADABLE: 'Die Planversion, an die dieses Abonnement gebunden ist ({planVersionId}), ist f\xFCr den Plan "{planKey}" nicht lesbar; deshalb kann kein Wechsel berechnet werden.',
1834
2706
  PLAN_NOT_IN_CATALOG: 'Plan "{planKey}" steht nicht im Katalog',
1835
2707
  PLAN_NOT_SELF_SERVICE: "{planName} wird nur \xFCber einen Sondervertrag freigeschaltet. Bitte wenden Sie sich an die Vertragsverwaltung.",
1836
2708
  PLAN_NOT_SOLD_IN_CYCLE: "{planName} hat f\xFCr diesen Abrechnungsrhythmus keinen Preis und kann darin nicht gebucht werden.",
@@ -1838,6 +2710,25 @@ var ERROR_MESSAGES_DE = {
1838
2710
  SUBSCRIPTION_CHANGED: "Dieses Abonnement hat sich ge\xE4ndert, w\xE4hrend die Anfrage entschieden wurde. Bitte lade es neu.",
1839
2711
  NO_SUBSCRIPTION: "Dieser Mandant hat kein Abonnement, das gek\xFCndigt werden kann.",
1840
2712
  CANCELLATION_TERMS_CHANGED: "Das Wirksamkeitsdatum hat sich seit der Anzeige ge\xE4ndert. Bitte best\xE4tige das neue Datum.",
2713
+ VERSION_OFFER_CHANGED: "Das Angebot hat sich seit der Anzeige ge\xE4ndert. Bitte sieh dir das aktuelle an, bevor du wechselst.",
2714
+ PLAN_CHANGE_QUOTE_CHANGED: "Dieser Plan hat sich seit der Anzeige ge\xE4ndert. Bitte sieh ihn dir noch einmal an, bevor du wechselst.",
2715
+ PLAN_CHANGE_VERSION_NOT_NAMED: "Ein Planwechsel muss die Version nennen, die seine Vorschau gezeigt hat.",
2716
+ VERSION_SWITCH_AFTER_CANCELLATION: "Dieses Abonnement endet, bevor diese Version wirksam w\xFCrde. Ein Wechsel dorthin ist nicht m\xF6glich.",
2717
+ VERSION_ENDS_BEFORE_SWITCH: "Diese Version wird nicht mehr verkauft, bevor sie wirksam w\xFCrde. Ein Wechsel dorthin ist nicht m\xF6glich.",
2718
+ RETIREMENT_TERMS_NOT_CONFIRMED: "Eine Version f\xFCr laufende Abonnements stillzulegen, braucht eine Klausel in deinen AGB. Setze tenantBilling.orderlyRetirement.termsConfirmed in config/saas.yaml, sobald sie sie enthalten.",
2719
+ RETIREMENT_VERSION_ON_SALE: "Version {version} von {planKey} wird noch verkauft. Beende zuerst ihren Verkauf, damit sie nach der Ank\xFCndigung niemand mehr bucht.",
2720
+ RETIREMENT_REPLACEMENT_NOT_ON_SALE: "Version {version} von {planKey} wird nicht verkauft. Abonnements k\xF6nnen nicht auf ihr weiterlaufen.",
2721
+ RETIREMENT_REPLACEMENT_NOT_SOLD_IN_RHYTHM: "{count} dieser Abonnements werden in einem Rhythmus abgerechnet, f\xFCr den Version {version} von {planKey} keinen Preis hat. Sie k\xF6nnen nicht auf ihr weiterlaufen.",
2722
+ RETIREMENT_REPLACEMENT_CANNOT_CARRY_BUNDLES: "{count} dieser Abonnements halten ein Bundle, das in Version {version} von {planKey} nicht laufen kann. Sie k\xF6nnen nicht auf ihr weiterlaufen.",
2723
+ RETIREMENT_REPLACEMENT_IS_RETIRED: "Eine Version kann nicht ihr eigener Ersatz sein.",
2724
+ RETIREMENT_NOTHING_AFFECTED: "Kein laufendes Abonnement nutzt Version {version} von {planKey}. Es gibt niemanden zu benachrichtigen.",
2725
+ RETIREMENT_WITHIN_TWELVE_MONTHS: "{count} dieser Abonnements waren in den letzten zw\xF6lf Monaten schon von einer Stilllegung betroffen. Ein Abonnement ist h\xF6chstens einmal im Jahr betroffen.",
2726
+ RETIREMENT_PREVIEW_CHANGED: "Die Abonnements, die diese Stilllegung betrifft, haben sich seit der Anzeige ge\xE4ndert. Sieh sie dir noch einmal an, bevor du sie ank\xFCndigst.",
2727
+ RETIREMENT_SWITCH_NOT_PENDING: "F\xFCr Ihre Version steht keine Einstellung mehr bevor. Es gibt nichts, wohin Sie wechseln k\xF6nnten.",
2728
+ RETIREMENT_SWITCH_IN_TRIAL: "Der Wechsel ist m\xF6glich, sobald Ihre Testphase endet.",
2729
+ RETIREMENT_SWITCH_BUNDLE_CANNOT_FOLLOW: "{bundleName} kann im Paket {planName} nicht laufen und l\xE4uft fr\xFChestens bis {until}. Ist es gek\xFCndigt, ist der Wechsel ab diesem Tag m\xF6glich.",
2730
+ RETIREMENT_SWITCH_NOT_OPEN: "Dieses Abonnement kann gerade nicht wechseln: Eine \xC4nderung ist geplant, es ist beendet, oder sein Paket gilt f\xFCr einen Sondervertrag.",
2731
+ RETIREMENT_SWITCH_CHANGED: "Die Einstellung hat sich seit der Anzeige ge\xE4ndert. Sehen Sie sie sich noch einmal an, bevor Sie wechseln.",
1841
2732
  SUBSCRIPTION_ENDED: "Dieses Abonnement ist beendet. Sein Paket kann nicht mehr gewechselt werden.",
1842
2733
  PLAN_LOCKED: "Aktiver Sondervertrag {planName} \u2014 f\xFCr einen Paketwechsel wenden Sie sich bitte an die Vertragsverwaltung.",
1843
2734
  QUOTA_OVER_TARGET: "Aktuelle Nutzung {used} \xFCberschreitet das Ziel-Limit {targetMax} ({quotaKey}) im Paket {planName}. Bitte zuerst die Nutzung reduzieren.",
@@ -1846,11 +2737,11 @@ var ERROR_MESSAGES_DE = {
1846
2737
  NO_CHANGE: "Zielpaket und Abrechnungsrhythmus entsprechen bereits dem aktuellen Stand.",
1847
2738
  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.",
1848
2739
  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.",
1849
- 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.",
2740
+ BUNDLE_BOOKING_OUTLASTS_TARGET_CYCLE: "{bundleName} wird j\xE4hrlich abgerechnet und l\xE4uft fr\xFChestens bis {until}; ein monatlich abgerechnetes Paket kann es nicht tragen. Ist es gek\xFCndigt, geht ein Wechsel durch, der an diesem Tag oder sp\xE4ter wirksam wird \u2014 oder behalten Sie den j\xE4hrlichen Rhythmus.",
2741
+ BUNDLE_BOOKING_DOES_NOT_FIT_TARGET_PLAN: "{bundleName} kann im Paket {planName} nicht laufen und l\xE4uft fr\xFChestens bis {until}. Ist es gek\xFCndigt, geht ein Wechsel durch, der an diesem Tag oder sp\xE4ter wirksam wird \u2014 oder w\xE4hlen Sie ein anderes Paket.",
1850
2742
  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.",
1851
2743
  MINIMUM_TERM_BINDS: "Die Mindestlaufzeit reicht \xFCber das Ende der Periode hinaus \u2014 die K\xFCndigung wird erst zum Ende der Mindestlaufzeit wirksam.",
1852
2744
  BUNDLE_FEATURE_DEPENDENCY_UNSATISFIED: "Das Bundle setzt [{features}] voraus \u2014 weder im Plan noch in den aktiven Bundles vorhanden.",
1853
- NO_PENDING_PLAN_VERSION: "Es wartet keine Planversion auf eine Best\xE4tigung.",
1854
2745
  ONBOARDING_CREATE_FAILED: "Das Konto konnte nicht angelegt werden. Bitte versuche es noch einmal.",
1855
2746
  BUNDLE_PREVIEW_ARGUMENT_AMBIGUOUS: "Genau eines von bundleVersionId (Vorschau f\xFCrs Buchen) oder subscriptionBundleId (Vorschau f\xFCrs K\xFCndigen) muss angegeben sein.",
1856
2747
  SUBSCRIPTION_PK_MISSING: "Der Adapter hat einen SubscriptionUsageRecord ohne id geliefert. Bitte den Prim\xE4rschl\xFCssel der Subscription durchreichen (siehe SubscriptionUsageRecord.id).",
@@ -1858,7 +2749,7 @@ var ERROR_MESSAGES_DE = {
1858
2749
  QUOTA_DIMENSION_UNKNOWN: 'Unbekannte Quota-Dimension "{dimension}".',
1859
2750
  // ── promo ──
1860
2751
  PROMO_CODE_NOT_FOUND: "Code nicht gefunden",
1861
- PROMO_CODE_ALREADY_EXISTS: "Diesen Code gibt es bereits.",
2752
+ PROMO_CODE_ALREADY_EXISTS: "Diesen Code gibt es bereits, oder ein gel\xF6schter Code tr\xE4gt ihn; ein gel\xF6schter Code beh\xE4lt seinen Namen.",
1862
2753
  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.",
1863
2754
  PROMO_CODE_NOT_REDEEMABLE: "Der Code l\xE4sst sich nicht einl\xF6sen: {reason}",
1864
2755
  PROMO_CODE_FORMAT_INVALID: 'Der Code darf nur Gro\xDFbuchstaben, Ziffern, "-" und "_" enthalten (4\u201332 Zeichen).',
@@ -1899,6 +2790,7 @@ var ERROR_MESSAGES_DE = {
1899
2790
  SUBSCRIPTION_CONTRACT_NOT_FOUND: "SubscriptionContract '{contractId}' nicht gefunden",
1900
2791
  NO_ACTIVE_SUBSCRIPTION_CONTRACT: "Kein aktiver Abo-Vertrag f\xFCr Mandant {tenantId}",
1901
2792
  SUBSCRIPTION_CONTRACT_ALREADY_CLOSED: "SubscriptionContract '{contractId}' ist bereits geschlossen",
2793
+ SUBSCRIPTION_CONTRACT_CHANGED: "Die Vertr\xE4ge von Mandant '{tenantId}' haben sich ge\xE4ndert, w\xE4hrend ein Nachfolger geschrieben wurde, oder einer beginnt nach dem Zeitpunkt, ab dem er g\xE4lte, sodass er neben einem anderen liefe. Es wurde nichts geschrieben.",
1902
2794
  // ── subscriber ──
1903
2795
  SUBSCRIBER_REQUIRED: "Mandant '{tenantId}' hat keinen Vertragspartner. Ohne ihn wird nichts vereinbart oder berechnet: Zuerst muss der Vertragspartner des Mandanten angelegt werden.",
1904
2796
  SUBSCRIBER_ALREADY_EXISTS: "Mandant '{tenantId}' hat bereits einen Vertragspartner.",
@@ -1934,7 +2826,16 @@ var ERROR_MESSAGES_DE = {
1934
2826
  // ── entitlement (code lives in upsell.types.ts) ──
1935
2827
  [FEATURE_NOT_LICENSED]: "Das Feature {featureKeys} ist im aktuellen Plan nicht enthalten.",
1936
2828
  // ── settings ──
1937
- SETTINGS_CHANGE_NOT_FOUND: "Keine aufgezeichnete Einstellungs\xE4nderung hat diese ID."
2829
+ SETTINGS_CHANGE_NOT_FOUND: "Keine aufgezeichnete Einstellungs\xE4nderung hat diese ID.",
2830
+ // ── maintenance ──
2831
+ MAINTENANCE: "Die Anwendung wird gerade gewartet.",
2832
+ MAINTENANCE_WINDOW_ALREADY_OPEN: "Es ist bereits ein Wartungsfenster offen. Verschiebe oder streiche es, bevor du ein weiteres ank\xFCndigst.",
2833
+ MAINTENANCE_WINDOW_NOT_OPEN: "Kein offenes Wartungsfenster hat diese ID.",
2834
+ MAINTENANCE_WINDOW_LOCKED: "Dieses Wartungsfenster ist gesperrt. Sein Beginn l\xE4sst sich nicht mehr verschieben, und beendet wird es durch Entsperren.",
2835
+ MAINTENANCE_WINDOW_END_NOT_AFTER_START: "Das Ende eines Wartungsfensters muss nach seinem Beginn liegen.",
2836
+ MAINTENANCE_WINDOW_END_IN_PAST: "Das Ende eines Wartungsfensters darf nicht in der Vergangenheit liegen.",
2837
+ MAINTENANCE_TIME_INVALID: "{field} muss Datum und Uhrzeit mit Zeitzone sein, etwa 2026-10-02T22:00+02:00.",
2838
+ MAINTENANCE_MESSAGE_TOO_LONG: "Die Nachricht darf h\xF6chstens {max} Zeichen lang sein."
1938
2839
  };
1939
2840
  // Annotate the CommonJS export names for ESM import in node:
1940
2841
  0 && (module.exports = {
@@ -1945,13 +2846,20 @@ var ERROR_MESSAGES_DE = {
1945
2846
  CATALOGUE_KEYS,
1946
2847
  CATALOG_ERROR_CODES,
1947
2848
  CONTRACT_ERROR_CODES,
2849
+ CUSTOM_LIMITS_KEYS,
1948
2850
  ERROR_MESSAGES_DE,
1949
2851
  ERROR_MESSAGES_EN,
1950
2852
  FEATURE_NOT_LICENSED,
1951
2853
  ForeignPaymentMethodReferenceError,
1952
2854
  LEGAL_IDENTITY_FIELDS,
2855
+ MAINTENANCE_CAPABILITY,
2856
+ MAINTENANCE_ERROR_CODES,
2857
+ MAINTENANCE_MESSAGE_MAX_LENGTH,
2858
+ MAINTENANCE_STATE_MAX_AGE_SECONDS,
1953
2859
  MARKETING_PRIORITY_MAX,
1954
2860
  MARKETING_PRIORITY_MIN,
2861
+ NO_SCHEDULED_CHANGE,
2862
+ OPTIONAL_CANONICAL_MODELS,
1955
2863
  OTP_RATE_LIMIT_MAX_SENDS,
1956
2864
  OTP_RATE_LIMIT_WINDOW_MINUTES,
1957
2865
  OTP_TTL_MINUTES,
@@ -1966,62 +2874,108 @@ var ERROR_MESSAGES_DE = {
1966
2874
  PROMO_ERROR_CODES,
1967
2875
  PaymentCallbackRejectedError,
1968
2876
  PersistenceCapabilityError,
2877
+ PersistenceRefusal,
1969
2878
  PlatformUserExistsError,
1970
2879
  REGISTRATION_ERROR_CODES,
1971
2880
  REGISTRATION_RESUME_TTL_MINUTES,
1972
2881
  REGISTRATION_STEP_BY_STATUS,
2882
+ RETIREMENT_REMINDER_LEAD_DAYS,
1973
2883
  SETTINGS_ERROR_CODES,
1974
2884
  SETUP_ERROR_CODES,
1975
2885
  SUBSCRIBER_ACCOUNT_CAPABILITY,
1976
2886
  SUBSCRIBER_ERROR_CODES,
2887
+ VERSION_RETIREMENT_CAPABILITY,
1977
2888
  applyPromo,
1978
2889
  assertPersistenceCapabilities,
1979
2890
  buildActivePlanVersionWhere,
1980
2891
  buildActiveVersionWhere,
1981
2892
  buildFeatureRequiresIndex,
1982
2893
  bundleDraftDefaults,
2894
+ bundleKeyTaken,
1983
2895
  bundleStemDefaults,
1984
2896
  canonicalJson,
2897
+ catalogDraftExists,
2898
+ catalogVersionAlreadyPublished,
2899
+ catalogVersionGone,
1985
2900
  classifyBundleVersionDiff,
1986
2901
  classifyIssuerChange,
1987
2902
  classifyPlanDiff,
2903
+ classifyVersionOffer,
1988
2904
  collectUnsatisfiedRequires,
2905
+ computeIncludedVat,
2906
+ concurrencyGate,
2907
+ concurrencyGateOf,
1989
2908
  contractPartiesOf,
1990
2909
  coverageExcludingSelf,
2910
+ customLimitsReader,
1991
2911
  definedFields,
1992
2912
  diffSettings,
1993
2913
  formatCustomerNumber,
1994
2914
  formatErrorMessage,
2915
+ grossFromNet,
1995
2916
  identityCorrectionDelta,
1996
2917
  isBundleRedundant,
1997
2918
  isForeignPaymentMethodReferenceError,
2919
+ isMaintenanceAnnouncementLapsed,
2920
+ isMaintenanceOverrun,
1998
2921
  isPaymentCallbackRejectedError,
2922
+ isPersistenceRefusal,
1999
2923
  isPlatformUserExistsError,
2924
+ isVersionActiveAt,
2000
2925
  isVersionEditable,
2001
2926
  issuerIdentityOf,
2002
2927
  keepOneRecommended,
2928
+ maintenanceRetryAfterSeconds,
2929
+ maintenanceStatusOf,
2930
+ maintenanceWindowStatusOf,
2931
+ maintenanceWindowViewOf,
2932
+ marketingProjectionTaken,
2933
+ membersLeftOut,
2003
2934
  missingRequiresFor,
2004
2935
  movedIdentityFields,
2936
+ netFromGross,
2937
+ noActivePlanVersion,
2938
+ parseZonedInstant,
2939
+ percentOf,
2005
2940
  pickActivePromo,
2006
2941
  planCatalogSettingsOf,
2942
+ planKeyTaken,
2943
+ planNotInCatalog,
2007
2944
  previousUtcDay,
2945
+ promoCodeTaken,
2008
2946
  promoStatus,
2009
2947
  promotionOnPrice,
2948
+ prorate,
2949
+ readCustomLimits,
2010
2950
  readQuotaRecord,
2011
2951
  readQuotaValue,
2012
2952
  recordedIssuerIdentity,
2013
2953
  refuseForeignPaymentMethodReference,
2014
2954
  resolveBundleAvailability,
2015
2955
  resolveErrorMessage,
2956
+ retirementCostsTheSubscription,
2957
+ retirementReminderDueAt,
2958
+ retirementReminderIsDue,
2959
+ retirementSwitchTerms,
2960
+ roundToCents,
2016
2961
  sameLegalIdentity,
2962
+ scheduledChangeAfterWrite,
2017
2963
  selectChargeableBundles,
2018
2964
  settingsSubtreeOf,
2019
2965
  startOfUtcDay,
2020
2966
  subscriberChargeColumns,
2021
2967
  subscriberFromRegistration,
2022
2968
  subscriberPaymentMethodColumns,
2969
+ subscriptionBundleAlreadyCancelled,
2970
+ subscriptionBundleGone,
2971
+ subscriptionContractGone,
2972
+ subscriptionGone,
2973
+ sumToCents,
2023
2974
  toBundleStemRow,
2975
+ toCents,
2024
2976
  toContractLineItemRecord,
2977
+ toDecimalString,
2978
+ toMaintenanceWindowRecord,
2025
2979
  toPlanRow,
2026
2980
  toPlanVersionRow,
2027
2981
  toRunningContractIssuer,
@@ -2030,5 +2984,10 @@ var ERROR_MESSAGES_DE = {
2030
2984
  toSubscriberPaymentMethodRecord,
2031
2985
  toSubscriberRecord,
2032
2986
  toSubscriptionBundleRecord,
2033
- toSubscriptionContractRecord
2987
+ toSubscriptionContractRecord,
2988
+ toSubscriptionNoticeRecord,
2989
+ toVersionRetirementRecord,
2990
+ versionOnSale,
2991
+ versionOnSaleOrNext,
2992
+ versionSale
2034
2993
  });