@saasicat/core 1.0.0-rc.6 → 1.0.0-rc.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -2,6 +2,11 @@
2
2
  function startOfUtcDay(date) {
3
3
  return new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()));
4
4
  }
5
+ function previousUtcDay(value) {
6
+ const result = new Date(value);
7
+ result.setUTCDate(result.getUTCDate() - 1);
8
+ return result;
9
+ }
5
10
  function buildActivePlanVersionWhere(asOf, options = {}) {
6
11
  const asOfDayStart = startOfUtcDay(asOf);
7
12
  const validityWindow = [
@@ -18,6 +23,53 @@ function buildActivePlanVersionWhere(asOf, options = {}) {
18
23
  }
19
24
  var buildActiveVersionWhere = buildActivePlanVersionWhere;
20
25
 
26
+ // src/bundle-draft-defaults.ts
27
+ function bundleDraftDefaults(data) {
28
+ return {
29
+ baseVersionId: data.baseVersionId ?? null,
30
+ features: data.features,
31
+ quotas: data.quotas ?? {},
32
+ compatibility: data.compatibility ?? {},
33
+ pricingOverrides: data.pricingOverrides ?? [],
34
+ monthlyNet: data.monthlyNet ?? null,
35
+ yearlyNet: data.yearlyNet ?? null,
36
+ marketed: data.marketed ?? true,
37
+ changeNote: data.changeNote ?? "",
38
+ createdByUserId: data.createdByUserId ?? null
39
+ };
40
+ }
41
+ function bundleStemDefaults(data) {
42
+ return {
43
+ bundleKey: data.bundleKey,
44
+ label: data.label,
45
+ description: data.description ?? null,
46
+ icon: data.icon ?? null,
47
+ sortOrder: data.sortOrder ?? 0,
48
+ i18n: data.i18n ?? {}
49
+ };
50
+ }
51
+ function toBundleStemRow(row) {
52
+ return {
53
+ id: row.id,
54
+ bundleKey: row.bundleKey,
55
+ label: row.label,
56
+ description: row.description,
57
+ icon: row.icon,
58
+ sortOrder: row.sortOrder,
59
+ i18n: row.i18n !== null && typeof row.i18n === "object" && !Array.isArray(row.i18n) ? row.i18n : {},
60
+ createdAt: row.createdAt.toISOString(),
61
+ updatedAt: row.updatedAt.toISOString(),
62
+ deletedAt: row.deletedAt?.toISOString() ?? null
63
+ };
64
+ }
65
+ function definedFields(data, keys) {
66
+ const patch = {};
67
+ for (const key of keys) {
68
+ if (data[key] !== void 0) patch[key] = data[key];
69
+ }
70
+ return patch;
71
+ }
72
+
21
73
  // src/catalog-entry.types.ts
22
74
  var MARKETING_PRIORITY_MIN = 0;
23
75
  var MARKETING_PRIORITY_MAX = 1e4;
@@ -273,6 +325,8 @@ var CATALOG_ERROR_CODES = {
273
325
  BUNDLE_VERSION_SUPERSEDED: "BUNDLE_VERSION_SUPERSEDED",
274
326
  BUNDLE_VERSION_REGRESSION: "BUNDLE_VERSION_REGRESSION",
275
327
  BUNDLE_VERSION_ZERO_PRICE: "BUNDLE_VERSION_ZERO_PRICE",
328
+ BUNDLE_VERSION_NO_PRICE: "BUNDLE_VERSION_NO_PRICE",
329
+ BUNDLE_VERSION_NOT_PRICED_FOR_PLAN: "BUNDLE_VERSION_NOT_PRICED_FOR_PLAN",
276
330
  BUNDLE_VERSION_DISCARD_NOT_IMPLEMENTED: "BUNDLE_VERSION_DISCARD_NOT_IMPLEMENTED",
277
331
  BUNDLE_VERSION_VALID_FROM_REQUIRED: "BUNDLE_VERSION_VALID_FROM_REQUIRED",
278
332
  BUNDLE_VERSION_VALID_FROM_INVALID: "BUNDLE_VERSION_VALID_FROM_INVALID",
@@ -319,6 +373,8 @@ var BILLING_ERROR_CODES = {
319
373
  BUNDLE_ALREADY_SUBSCRIBED: "BUNDLE_ALREADY_SUBSCRIBED",
320
374
  BUNDLE_INCOMPATIBLE_WITH_PLAN: "BUNDLE_INCOMPATIBLE_WITH_PLAN",
321
375
  BUNDLE_NOT_SELF_SERVICE: "BUNDLE_NOT_SELF_SERVICE",
376
+ BUNDLE_CYCLE_EXCEEDS_PLAN: "BUNDLE_CYCLE_EXCEEDS_PLAN",
377
+ BUNDLE_NOT_PRICED_FOR_THIS_PLAN: "BUNDLE_NOT_PRICED_FOR_THIS_PLAN",
322
378
  SUBSCRIPTION_BUNDLE_ALREADY_CANCELLED: "SUBSCRIPTION_BUNDLE_ALREADY_CANCELLED",
323
379
  SUBSCRIPTION_BUNDLE_NOT_CANCELLED: "SUBSCRIPTION_BUNDLE_NOT_CANCELLED",
324
380
  SUBSCRIPTION_BUNDLE_CANCELLATION_EFFECTIVE: "SUBSCRIPTION_BUNDLE_CANCELLATION_EFFECTIVE",
@@ -335,6 +391,42 @@ var BILLING_ERROR_CODES = {
335
391
  PLAN_NOT_SELF_SERVICE: "PLAN_NOT_SELF_SERVICE",
336
392
  /** Plan change refused. Carries `blockers[]` with their own codes. */
337
393
  PLAN_CHANGE_BLOCKED: "PLAN_CHANGE_BLOCKED",
394
+ // ── the plan-change preview's own blockers and warnings ──
395
+ //
396
+ // These travel inside a 200 response, in `blockers[]` and `warnings[]`, not
397
+ // as thrown errors — but they are read by a person and so belong in the same
398
+ // catalogue. Each carries `params` with the values its sentence names.
399
+ //
400
+ // Three of them used to construct their code from the subject:
401
+ // `VEHICLES_OVER_TARGET`, `ENTERPRISE_LOCKED`, `PRO_NOT_SELF_SERVICE`. That
402
+ // made the set grow with every quota and plan an installation defines, so no
403
+ // catalogue could be complete against it and no guard could check one. The
404
+ // subject is a parameter now.
405
+ /** The subscription has ended; its plan can no longer be changed. */
406
+ SUBSCRIPTION_ENDED: "SUBSCRIPTION_ENDED",
407
+ /** An active special contract blocks self-service plan changes. */
408
+ PLAN_LOCKED: "PLAN_LOCKED",
409
+ /** Current usage of one quota exceeds what the target plan allows. */
410
+ QUOTA_OVER_TARGET: "QUOTA_OVER_TARGET",
411
+ /** The change drops features the tenant has today. */
412
+ FEATURE_LOST: "FEATURE_LOST",
413
+ FEATURES_LOST: "FEATURES_LOST",
414
+ /** Target plan and cycle already match what is in place. */
415
+ NO_CHANGE: "NO_CHANGE",
416
+ /** A shorter cycle cannot start inside the term already running. */
417
+ CYCLE_SHORTENS_AT_TERM_END: "CYCLE_SHORTENS_AT_TERM_END",
418
+ /** A cancelled subscription cannot change its billing cycle. */
419
+ CANCELLATION_LOCKS_THE_CYCLE: "CANCELLATION_LOCKS_THE_CYCLE",
420
+ /**
421
+ * A bundle the tenant already holds runs past the cycle they are moving to.
422
+ *
423
+ * Its own code rather than `BUNDLE_CYCLE_EXCEEDS_PLAN`, which states the
424
+ * same rule about a booking that has not been made yet. The two need
425
+ * different sentences: this one can name the day the obstacle lifts and
426
+ * tell the reader to cancel the booking, and that advice is wrong for
427
+ * someone who is only about to book. One template cannot serve both.
428
+ */
429
+ BUNDLE_BOOKING_OUTLASTS_TARGET_CYCLE: "BUNDLE_BOOKING_OUTLASTS_TARGET_CYCLE",
338
430
  NO_PENDING_PLAN_VERSION: "NO_PENDING_PLAN_VERSION",
339
431
  ONBOARDING_CREATE_FAILED: "ONBOARDING_CREATE_FAILED",
340
432
  BUNDLE_PREVIEW_ARGUMENT_AMBIGUOUS: "BUNDLE_PREVIEW_ARGUMENT_AMBIGUOUS",
@@ -525,6 +617,137 @@ var REGISTRATION_STEP_BY_STATUS = {
525
617
  };
526
618
  var REGISTRATION_RESUME_TTL_MINUTES = 60;
527
619
 
620
+ // src/plan-mapping.ts
621
+ function toPlanRow(row) {
622
+ return {
623
+ id: row.id,
624
+ planKey: row.planKey,
625
+ label: row.label,
626
+ description: row.description,
627
+ icon: row.icon,
628
+ sortOrder: row.sortOrder,
629
+ createdAt: row.createdAt.toISOString(),
630
+ updatedAt: row.updatedAt.toISOString(),
631
+ deletedAt: toIsoOrNull(row.deletedAt)
632
+ };
633
+ }
634
+ function toPlanVersionRow(row, planKey, fields) {
635
+ const mapped = {
636
+ id: row.id,
637
+ version: row.version,
638
+ baseVersionId: row.baseVersionId,
639
+ planId: planKey,
640
+ features: toFeatureKeys(row.features),
641
+ quotas: toQuotaEffects(row.quotas),
642
+ monthlyNet: String(row.monthlyNet),
643
+ yearlyNet: String(row.yearlyNet),
644
+ marketed: row.marketed,
645
+ publishedAt: toIsoOrNull(row.publishedAt),
646
+ supersededAt: toIsoOrNull(row.supersededAt),
647
+ publishedChanges: Array.isArray(row.publishedChanges) ? row.publishedChanges : null,
648
+ changeNote: row.changeNote,
649
+ nonRegressive: row.nonRegressive,
650
+ validFrom: fields.validityWindows ? toIsoOrNull(row.validFrom) : null,
651
+ validUntil: fields.validityWindows ? toIsoOrNull(row.validUntil) : null,
652
+ createdByUserId: row.createdByUserId,
653
+ publishedByUserId: row.publishedByUserId,
654
+ createdAt: row.createdAt.toISOString(),
655
+ updatedAt: row.updatedAt.toISOString()
656
+ };
657
+ if (fields.endsAt) {
658
+ mapped.endsAt = toIsoOrNull(row.endsAt);
659
+ }
660
+ return mapped;
661
+ }
662
+ function toIsoOrNull(value) {
663
+ return value ? value.toISOString() : null;
664
+ }
665
+ function toFeatureKeys(value) {
666
+ if (!Array.isArray(value)) return [];
667
+ return value.filter((entry) => typeof entry === "string");
668
+ }
669
+ function toQuotaEffects(value) {
670
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
671
+ return {};
672
+ }
673
+ const quotas = {};
674
+ for (const [key, entry] of Object.entries(value)) {
675
+ if (typeof entry === "number") quotas[key] = entry;
676
+ }
677
+ return quotas;
678
+ }
679
+
680
+ // src/subscription-contract-mapping.ts
681
+ function toSubscriptionContractRecord(row, lineItems) {
682
+ return {
683
+ id: row.id,
684
+ tenantId: row.tenantId,
685
+ status: row.status,
686
+ effectiveFrom: row.effectiveFrom,
687
+ effectiveUntil: row.effectiveUntil,
688
+ originalOfferId: row.originalOfferId,
689
+ originalPlanVersionId: row.originalPlanVersionId,
690
+ originalBundleVersionIds: toStringArray(row.originalBundleVersionIds),
691
+ entitlementSnapshot: isPlainObject(row.entitlementSnapshot) ? row.entitlementSnapshot : null,
692
+ priceSnapshot: row.priceSnapshot,
693
+ promotionSnapshots: toUnknownArray(row.promotionSnapshots),
694
+ promoCodeSnapshots: toUnknownArray(row.promoCodeSnapshots),
695
+ termsSnapshot: toRecordOrNull(row.termsSnapshot),
696
+ lineItems: lineItems.map(toContractLineItemRecord),
697
+ createdAt: row.createdAt,
698
+ updatedAt: row.updatedAt
699
+ };
700
+ }
701
+ function toContractLineItemRecord(row) {
702
+ return {
703
+ id: row.id,
704
+ contractId: row.contractId,
705
+ kind: row.kind,
706
+ sourceKey: row.sourceKey,
707
+ sourceVersionId: row.sourceVersionId,
708
+ titleSnapshot: row.titleSnapshot,
709
+ descriptionSnapshot: row.descriptionSnapshot,
710
+ quantity: row.quantity,
711
+ unit: row.unit,
712
+ priceNet: Number(row.priceNet),
713
+ priceGross: Number(row.priceGross),
714
+ billingCycle: row.billingCycle,
715
+ minimumTermUntil: row.minimumTermUntil,
716
+ featuresSnapshot: toStringArray(row.featuresSnapshot),
717
+ quotaEffectsSnapshot: toQuotaEffects2(row.quotaEffectsSnapshot),
718
+ metadata: toRecordOrNull(row.metadata),
719
+ createdAt: row.createdAt
720
+ };
721
+ }
722
+ function isPlainObject(value) {
723
+ return value !== null && typeof value === "object" && !Array.isArray(value);
724
+ }
725
+ function toUnknownArray(value) {
726
+ return Array.isArray(value) ? value : [];
727
+ }
728
+ function toRecordOrNull(value) {
729
+ return isPlainObject(value) ? value : null;
730
+ }
731
+ function toStringArray(value) {
732
+ if (!Array.isArray(value)) return [];
733
+ return value.filter((entry) => typeof entry === "string");
734
+ }
735
+ function toQuotaEffects2(value) {
736
+ if (!isPlainObject(value)) return {};
737
+ const effects = {};
738
+ for (const [key, entry] of Object.entries(value)) {
739
+ if (typeof entry === "number") effects[key] = entry;
740
+ }
741
+ return effects;
742
+ }
743
+
744
+ // src/subscription-contract.types.ts
745
+ var BUNDLE_PRICE_LOOKUP_LIMIT = 200;
746
+ var ACTIVE_SUBSCRIPTION_CONTRACT_STATUSES = [
747
+ "active",
748
+ "scheduled"
749
+ ];
750
+
528
751
  // src/version-editability.ts
529
752
  function isVersionEditable(v, now = /* @__PURE__ */ new Date()) {
530
753
  if (v.publishedAt === null) {
@@ -596,6 +819,8 @@ var ERROR_MESSAGES_EN = {
596
819
  BUNDLE_VERSION_SUPERSEDED: "BundleVersion '{bundleVersionId}' has been superseded by a newer version.",
597
820
  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).",
598
821
  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.",
822
+ BUNDLE_VERSION_NO_PRICE: "This bundle version cannot be published without a price: neither a base price nor any plan override resolves one.",
823
+ 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.",
599
824
  BUNDLE_VERSION_DISCARD_NOT_IMPLEMENTED: "Discard is not implemented in the current repository. Implement BundleRepository.deleteDraft.",
600
825
  BUNDLE_VERSION_VALID_FROM_REQUIRED: "Published bundle versions must keep a validFrom. Set a new future date, or edit the draft before publishing.",
601
826
  BUNDLE_VERSION_VALID_FROM_INVALID: "validFrom '{validFrom}' is not a valid date",
@@ -609,12 +834,12 @@ var ERROR_MESSAGES_EN = {
609
834
  PLAN_VERSION_NOT_FOUND: "PlanVersion '{versionId}' not found",
610
835
  BUNDLE_NOT_FOUND: "Bundle '{bundleId}' not found",
611
836
  BUNDLE_VERSION_NOT_FOUND: "BundleVersion '{bundleVersionId}' not found",
612
- FEATURE_NOT_FOUND: "Feature '{featureKey}' not found in project '{projectKey}'",
613
- QUOTA_NOT_FOUND: "Quota '{quotaKey}' not found in project '{projectKey}'",
837
+ FEATURE_NOT_FOUND: "Feature '{featureKey}' not found",
838
+ QUOTA_NOT_FOUND: "Quota '{quotaKey}' not found",
614
839
  PROMOTION_NOT_FOUND: "Promotion '{promotionId}' not found",
615
840
  MARKETING_PROJECTION_NOT_FOUND: "MarketingProjection '{projectionId}' not found",
616
- PLAN_ALREADY_EXISTS: "Plan '{planKey}' already exists in project '{projectKey}'",
617
- BUNDLE_ALREADY_EXISTS: "Bundle '{bundleKey}' already exists in project '{projectKey}'",
841
+ PLAN_ALREADY_EXISTS: "Plan '{planKey}' already exists",
842
+ BUNDLE_ALREADY_EXISTS: "Bundle '{bundleKey}' already exists",
618
843
  MARKETING_PROJECTION_ALREADY_EXISTS: "Marketing projection for {targetType}/{targetVersionId}/{locale} already exists \u2014 use PATCH to edit it",
619
844
  PLAN_DRAFT_ALREADY_EXISTS: "Plan '{planKey}' already has a draft version v{draftVersion}; publish or discard it first",
620
845
  BUNDLE_DRAFT_ALREADY_EXISTS: "Bundle '{bundleKey}' already has a draft version v{draftVersion}; publish or discard it first",
@@ -628,6 +853,8 @@ var ERROR_MESSAGES_EN = {
628
853
  BUNDLE_ALREADY_SUBSCRIBED: "Subscription '{subscriptionId}' has already actively booked this bundle.",
629
854
  BUNDLE_INCOMPATIBLE_WITH_PLAN: "BundleVersion '{bundleVersionId}' is not compatible with plan '{planKey}'. Allowed: [{allowedPlanKeys}].",
630
855
  BUNDLE_NOT_SELF_SERVICE: "Bundle '{bundleKey}' is only activated via a special contract. Please contact the contract manager.",
856
+ 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.",
857
+ BUNDLE_NOT_PRICED_FOR_THIS_PLAN: "This bundle has no {billingCycle} price for the {planKey} plan, so it cannot be booked from here.",
631
858
  SUBSCRIPTION_BUNDLE_ALREADY_CANCELLED: "SubscriptionBundle '{subscriptionBundleId}' is already cancelled.",
632
859
  SUBSCRIPTION_BUNDLE_NOT_CANCELLED: "SubscriptionBundle '{subscriptionBundleId}' is not cancelled.",
633
860
  SUBSCRIPTION_BUNDLE_CANCELLATION_EFFECTIVE: "Cancellation already in effect \u2014 book the bundle again.",
@@ -636,9 +863,18 @@ var ERROR_MESSAGES_EN = {
636
863
  SUBSCRIPTION_BUNDLE_NOT_FOUND: "SubscriptionBundle '{subscriptionBundleId}' not found",
637
864
  TENANT_NOT_FOUND: "Tenant {slug} not found",
638
865
  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.",
639
- PLAN_NOT_IN_CATALOG: 'Plan "{planKey}" is not in the catalog ({projectKey})',
640
- PLAN_NOT_SELF_SERVICE: "{planKey} is not activated via self-service.",
866
+ PLAN_NOT_IN_CATALOG: 'Plan "{planKey}" is not in the catalog',
867
+ PLAN_NOT_SELF_SERVICE: "{planName} is only activated via a special contract. Please contact the contract manager.",
641
868
  PLAN_CHANGE_BLOCKED: "Plan change during onboarding is blocked.",
869
+ SUBSCRIPTION_ENDED: "This subscription has ended. Its plan can no longer be changed.",
870
+ PLAN_LOCKED: "Active {planName} special contract \u2014 please contact the contract manager to change plans.",
871
+ QUOTA_OVER_TARGET: "Current usage {used} exceeds the target limit {targetMax} ({quotaKey}) in the {planName} plan. Please reduce usage first.",
872
+ FEATURE_LOST: "Switching means losing access to one feature. Existing data is retained and never deleted \u2014 upgrading again unlocks it.",
873
+ FEATURES_LOST: "Switching means losing access to {count} features. Existing data is retained and never deleted \u2014 upgrading again unlocks it.",
874
+ NO_CHANGE: "Target plan and billing cycle already match the current state.",
875
+ 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.",
876
+ 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.",
877
+ 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.",
642
878
  NO_PENDING_PLAN_VERSION: "There is no pending plan version awaiting confirmation.",
643
879
  ONBOARDING_CREATE_FAILED: "The account could not be created. Please try again.",
644
880
  BUNDLE_PREVIEW_ARGUMENT_AMBIGUOUS: "Exactly one of bundleVersionId (add preview) or subscriptionBundleId (cancel preview) must be given.",
@@ -756,6 +992,8 @@ var ERROR_MESSAGES_DE = {
756
992
  BUNDLE_VERSION_SUPERSEDED: "BundleVersion '{bundleVersionId}' wurde durch eine neuere Version abgel\xF6st.",
757
993
  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).",
758
994
  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.",
995
+ BUNDLE_VERSION_NO_PRICE: "Diese Bundle-Version kann ohne Preis nicht ver\xF6ffentlicht werden: weder ein Grundpreis noch eine Plan-\xDCberschreibung ergibt einen.",
996
+ 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.",
759
997
  BUNDLE_VERSION_DISCARD_NOT_IMPLEMENTED: "Das Verwerfen ist im aktuellen Repository nicht implementiert. Implementiere BundleRepository.deleteDraft.",
760
998
  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.",
761
999
  BUNDLE_VERSION_VALID_FROM_INVALID: "validFrom '{validFrom}' ist kein g\xFCltiges Datum",
@@ -769,12 +1007,12 @@ var ERROR_MESSAGES_DE = {
769
1007
  PLAN_VERSION_NOT_FOUND: "PlanVersion '{versionId}' nicht gefunden",
770
1008
  BUNDLE_NOT_FOUND: "Bundle '{bundleId}' nicht gefunden",
771
1009
  BUNDLE_VERSION_NOT_FOUND: "BundleVersion '{bundleVersionId}' nicht gefunden",
772
- FEATURE_NOT_FOUND: "Feature '{featureKey}' im Projekt '{projectKey}' nicht gefunden",
773
- QUOTA_NOT_FOUND: "Quota '{quotaKey}' im Projekt '{projectKey}' nicht gefunden",
1010
+ FEATURE_NOT_FOUND: "Feature '{featureKey}' nicht gefunden",
1011
+ QUOTA_NOT_FOUND: "Quota '{quotaKey}' nicht gefunden",
774
1012
  PROMOTION_NOT_FOUND: "Promotion '{promotionId}' nicht gefunden",
775
1013
  MARKETING_PROJECTION_NOT_FOUND: "MarketingProjection '{projectionId}' nicht gefunden",
776
- PLAN_ALREADY_EXISTS: "Plan '{planKey}' gibt es im Projekt '{projectKey}' bereits",
777
- BUNDLE_ALREADY_EXISTS: "Bundle '{bundleKey}' gibt es im Projekt '{projectKey}' bereits",
1014
+ PLAN_ALREADY_EXISTS: "Plan '{planKey}' gibt es bereits",
1015
+ BUNDLE_ALREADY_EXISTS: "Bundle '{bundleKey}' gibt es bereits",
778
1016
  MARKETING_PROJECTION_ALREADY_EXISTS: "F\xFCr {targetType}/{targetVersionId}/{locale} gibt es bereits eine Marketing-Projektion \u2014 bearbeite sie per PATCH",
779
1017
  PLAN_DRAFT_ALREADY_EXISTS: "Plan '{planKey}' hat bereits die Entwurfsversion v{draftVersion}; ver\xF6ffentliche oder verwirf sie zuerst",
780
1018
  BUNDLE_DRAFT_ALREADY_EXISTS: "Bundle '{bundleKey}' hat bereits die Entwurfsversion v{draftVersion}; ver\xF6ffentliche oder verwirf sie zuerst",
@@ -788,6 +1026,8 @@ var ERROR_MESSAGES_DE = {
788
1026
  BUNDLE_ALREADY_SUBSCRIBED: "Abonnement '{subscriptionId}' hat dieses Bundle bereits aktiv gebucht.",
789
1027
  BUNDLE_INCOMPATIBLE_WITH_PLAN: "BundleVersion '{bundleVersionId}' passt nicht zum Plan '{planKey}'. Erlaubt: [{allowedPlanKeys}].",
790
1028
  BUNDLE_NOT_SELF_SERVICE: "Bundle '{bundleKey}' wird nur \xFCber einen Sondervertrag freigeschaltet. Bitte wende dich an die Vertragsverwaltung.",
1029
+ 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.",
1030
+ 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.",
791
1031
  SUBSCRIPTION_BUNDLE_ALREADY_CANCELLED: "SubscriptionBundle '{subscriptionBundleId}' ist bereits gek\xFCndigt.",
792
1032
  SUBSCRIPTION_BUNDLE_NOT_CANCELLED: "SubscriptionBundle '{subscriptionBundleId}' ist nicht gek\xFCndigt.",
793
1033
  SUBSCRIPTION_BUNDLE_CANCELLATION_EFFECTIVE: "Die K\xFCndigung ist bereits wirksam \u2014 buche das Bundle einfach neu.",
@@ -796,9 +1036,18 @@ var ERROR_MESSAGES_DE = {
796
1036
  SUBSCRIPTION_BUNDLE_NOT_FOUND: "SubscriptionBundle '{subscriptionBundleId}' nicht gefunden",
797
1037
  TENANT_NOT_FOUND: "Mandant {slug} nicht gefunden",
798
1038
  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.",
799
- PLAN_NOT_IN_CATALOG: 'Plan "{planKey}" steht nicht im Katalog ({projectKey})',
800
- PLAN_NOT_SELF_SERVICE: "{planKey} wird nicht per Self-Service freigeschaltet.",
1039
+ PLAN_NOT_IN_CATALOG: 'Plan "{planKey}" steht nicht im Katalog',
1040
+ PLAN_NOT_SELF_SERVICE: "{planName} wird nur \xFCber einen Sondervertrag freigeschaltet. Bitte wenden Sie sich an die Vertragsverwaltung.",
801
1041
  PLAN_CHANGE_BLOCKED: "W\xE4hrend des Onboardings ist ein Planwechsel gesperrt.",
1042
+ SUBSCRIPTION_ENDED: "Dieses Abonnement ist beendet. Sein Paket kann nicht mehr gewechselt werden.",
1043
+ PLAN_LOCKED: "Aktiver Sondervertrag {planName} \u2014 f\xFCr einen Paketwechsel wenden Sie sich bitte an die Vertragsverwaltung.",
1044
+ QUOTA_OVER_TARGET: "Aktuelle Nutzung {used} \xFCberschreitet das Ziel-Limit {targetMax} ({quotaKey}) im Paket {planName}. Bitte zuerst die Nutzung reduzieren.",
1045
+ FEATURE_LOST: "Beim Wechsel verlieren Sie den Zugriff auf eine Funktion. Vorhandene Daten bleiben erhalten und werden nie gel\xF6scht \u2014 ein sp\xE4teres Upgrade schaltet sie wieder frei.",
1046
+ FEATURES_LOST: "Beim Wechsel verlieren Sie den Zugriff auf {count} Funktionen. Vorhandene Daten bleiben erhalten und werden nie gel\xF6scht \u2014 ein sp\xE4teres Upgrade schaltet sie wieder frei.",
1047
+ NO_CHANGE: "Zielpaket und Abrechnungsrhythmus entsprechen bereits dem aktuellen Stand.",
1048
+ 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.",
1049
+ 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.",
1050
+ 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.",
802
1051
  NO_PENDING_PLAN_VERSION: "Es wartet keine Planversion auf eine Best\xE4tigung.",
803
1052
  ONBOARDING_CREATE_FAILED: "Das Konto konnte nicht angelegt werden. Bitte versuche es noch einmal.",
804
1053
  BUNDLE_PREVIEW_ARGUMENT_AMBIGUOUS: "Genau eines von bundleVersionId (Vorschau f\xFCrs Buchen) oder subscriptionBundleId (Vorschau f\xFCrs K\xFCndigen) muss angegeben sein.",
@@ -857,8 +1106,10 @@ var ERROR_MESSAGES_DE = {
857
1106
  [FEATURE_NOT_LICENSED]: "Das Feature {featureKeys} ist im aktuellen Plan nicht enthalten."
858
1107
  };
859
1108
  export {
1109
+ ACTIVE_SUBSCRIPTION_CONTRACT_STATUSES,
860
1110
  AUTH_ERROR_CODES,
861
1111
  BILLING_ERROR_CODES,
1112
+ BUNDLE_PRICE_LOOKUP_LIMIT,
862
1113
  CATALOG_ERROR_CODES,
863
1114
  CONTRACT_ERROR_CODES,
864
1115
  ERROR_MESSAGES_DE,
@@ -887,19 +1138,28 @@ export {
887
1138
  buildActivePlanVersionWhere,
888
1139
  buildActiveVersionWhere,
889
1140
  buildFeatureRequiresIndex,
1141
+ bundleDraftDefaults,
1142
+ bundleStemDefaults,
890
1143
  classifyBundleVersionDiff,
891
1144
  classifyPlanDiff,
892
1145
  collectUnsatisfiedRequires,
893
1146
  coverageExcludingSelf,
1147
+ definedFields,
894
1148
  formatErrorMessage,
895
1149
  isBundleRedundant,
896
1150
  isPlatformUserExistsError,
897
1151
  isVersionEditable,
898
1152
  missingRequiresFor,
899
1153
  pickActivePromo,
1154
+ previousUtcDay,
900
1155
  promoStatus,
901
1156
  resolveBundleAvailability,
902
1157
  resolveErrorMessage,
903
1158
  selectChargeableBundles,
904
- startOfUtcDay
1159
+ startOfUtcDay,
1160
+ toBundleStemRow,
1161
+ toContractLineItemRecord,
1162
+ toPlanRow,
1163
+ toPlanVersionRow,
1164
+ toSubscriptionContractRecord
905
1165
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saasicat/core",
3
- "version": "1.0.0-rc.6",
3
+ "version": "1.0.0-rc.8",
4
4
  "description": "TypeScript contracts for SaaSiCat — capabilities, catalogs, subscriptions, entitlements, admin manifests, persistence ports — and the pure domain logic both server and browser run: error catalogues, plan-diff classification, promo arithmetic, feature requirements.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",