@saasicat/core 1.0.0-rc.0 → 1.0.0-rc.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,5 +1,17 @@
1
1
  /** Start of day (00:00 UTC) of the moment — for day-inclusive date comparisons. */
2
2
  declare function startOfUtcDay(date: Date): Date;
3
+ /**
4
+ * The day before `value` — how a validity window closes when its successor
5
+ * opens (`validUntil = successor.validFrom − 1 day`, the rule this module's
6
+ * header states and `startOfUtcDay` above is the reading half of).
7
+ *
8
+ * It lived as three separate expressions before 2026-08-27: one in each
9
+ * adapter's publish path and one in the bundle repository, all agreeing by
10
+ * coincidence rather than by construction. Day arithmetic rather than
11
+ * `− 24 * 60 * 60 * 1000`: the subtraction is only equivalent while the value
12
+ * is a UTC midnight, and nothing in the type says it is.
13
+ */
14
+ declare function previousUtcDay(value: Date): Date;
3
15
  /** `<=` upper bound for a date (structurally Prisma-compatible). */
4
16
  interface DateAtOrBefore {
5
17
  lte: Date;
@@ -73,256 +85,6 @@ type ActiveVersionWhereWithEndsAt = ActivePlanVersionWhereWithEndsAt;
73
85
  */
74
86
  declare const buildActiveVersionWhere: typeof buildActivePlanVersionWhere;
75
87
 
76
- type FeatureKey = string;
77
- type PlanId = string;
78
- type QuotaKey = string;
79
- interface FeatureDef {
80
- key: FeatureKey;
81
- label?: string;
82
- icon?: string;
83
- /** CORE / ADVANCED / PRO / BUSINESS / ENTERPRISE_ONLY — convention. */
84
- tier?: string;
85
- plannedOnly?: boolean;
86
- }
87
- interface PlanDef {
88
- id: PlanId;
89
- name?: string;
90
- tagline?: string;
91
- /** false = not selectable in self-service onboarding. Default: true. */
92
- marketed?: boolean;
93
- /** Highlighted card in onboarding (max. 1 per catalog). */
94
- popular?: boolean;
95
- /** Net monthly price. null = on request. */
96
- monthlyNet?: number | null;
97
- /** Net total amount per year. null = monthly only. */
98
- yearlyNet?: number | null;
99
- /** Map quotaKey → max value. -1 = unlimited. */
100
- quotas: Record<QuotaKey, number>;
101
- features: FeatureKey[];
102
- }
103
- /** App-wide marketing configuration. */
104
- interface PlanCatalogMarketing {
105
- /**
106
- * Allowed language pool that the app may market. First = default
107
- * locale. From it, the SuperAdmin activates a subset in the marketing
108
- * catalog (LocaleManager).
109
- */
110
- availableLocales: string[];
111
- }
112
- /**
113
- * App identity block for branding + version. Consumed by the `AdminPublicBootController`
114
- * and the `AdminManifestConfigFactory`; the SuperAdmin UI (platform
115
- * LoginPage, AdminLayout brand block) reads the same fields via PublicBoot.
116
- *
117
- * `name` = brand display name (e.g. "DemoApp", "ClubApp").
118
- * `label` = tag/subtitle in the brand block (e.g. "SuperAdmin").
119
- * `version` = app version string (build info).
120
- * `icon` = 2-character abbreviation for the logo badge (e.g. "ma", "da").
121
- * `logoUrl` = optional URL to a PNG/SVG; if set, the UI renders an <img>
122
- * instead of the initials badge.
123
- */
124
- interface PlanCatalogApp {
125
- name: string;
126
- label?: string;
127
- version?: string;
128
- icon?: string;
129
- logoUrl?: string;
130
- }
131
- interface PlanCatalog {
132
- schemaVersion: 1;
133
- projectKey: string;
134
- /** App identity (branding + version), see PlanCatalogApp. Optional. */
135
- app?: PlanCatalogApp;
136
- /** ISO-4217 currency code. */
137
- currency: string;
138
- /** VAT rate in percent. */
139
- vatRate: number;
140
- /** App-wide marketing configuration. Optional. */
141
- marketing?: PlanCatalogMarketing;
142
- features?: FeatureDef[];
143
- /**
144
- * Optional. When omitted, plans come exclusively from the
145
- * AdminUI / DB table (Plans/PlanVersions lifecycle).
146
- */
147
- plans?: PlanDef[];
148
- }
149
-
150
- /** Backend capability key, convention: domain.action[.action]. */
151
- type CapabilityKey = string;
152
- /** Frontend action-registry key. Same convention as CapabilityKey. */
153
- type ActionKey = CapabilityKey;
154
- /** Lookup key in the static extensions: map of the UI build. */
155
- type ComponentKey = string;
156
- interface AdminManifest {
157
- schemaVersion: 1;
158
- project: {
159
- key: string;
160
- displayName: string;
161
- /** Tag/subtitle (e.g. "SuperAdmin"). From `saas.yaml#app.label`. */
162
- label?: string;
163
- /** Short abbreviation for the logo badge (e.g. "ma", "da"). From `saas.yaml#app.icon`. */
164
- icon?: string;
165
- logoUrl?: string;
166
- environment?: 'production' | 'staging' | 'development';
167
- /**
168
- * Allowed locale pool from the app config (`saas.yaml`
169
- * `marketing.availableLocales`). First = default..
170
- */
171
- availableLocales?: string[];
172
- /** Default locale; equals `availableLocales[0]`. */
173
- defaultLocale?: string;
174
- };
175
- build: {
176
- platformPackageVersion: string;
177
- appVersion: string;
178
- manifestHash: string;
179
- };
180
- planCatalogSnapshot: {
181
- source: string;
182
- hash: string;
183
- currency: string;
184
- vatRate: number;
185
- features?: FeatureDef[];
186
- plans: PlanDef[];
187
- };
188
- /** Map CapabilityKey → boolean. Manifest is never a security source. */
189
- capabilities: Record<CapabilityKey, boolean>;
190
- navigation: {
191
- standardPages: Partial<Record<StandardPageKey, StandardPageDef>>;
192
- projectPages?: ProjectPageDef[];
193
- };
194
- dashboard?: {
195
- kpiCards?: KpiCardDef[];
196
- };
197
- tenants?: {
198
- columns?: TenantColumnDef[];
199
- actions?: TenantActionDef[];
200
- };
201
- audit?: {
202
- actions?: AuditActionDef[];
203
- };
204
- }
205
- type StandardPageKey = 'dashboard' | 'tenants' | 'subscriptions' | 'promoCodes' | 'plans' | 'audit' | 'users' | 'pilots' | 'discovery' | 'bundles' | 'marketingCatalog' | 'platformEmail' | 'platformEmailHistory';
206
- interface StandardPageDef {
207
- enabled: boolean;
208
- requiredCapability?: CapabilityKey;
209
- }
210
- interface ProjectPageDef {
211
- /** `<projectKey>.<area>`, e.g. `demoapp.datev`. */
212
- id: string;
213
- label: string;
214
- icon?: string;
215
- /** Frontend route, e.g. `/admin/datev`. */
216
- route: string;
217
- navSection?: string;
218
- /** Lookup in the static extensions: map of the shell build. */
219
- componentKey: ComponentKey;
220
- requiredCapability?: CapabilityKey;
221
- prefetchOnIdle?: boolean;
222
- }
223
- interface KpiCardDef {
224
- id: string;
225
- label: string;
226
- /** Required path: /api/v1/admin/(extras|dashboard)/... */
227
- endpoint: string;
228
- displayHint: KpiDisplayHint;
229
- /** 0–100; UI sorts descending. */
230
- slotPriority?: number;
231
- requiredCapability?: CapabilityKey;
232
- }
233
- interface KpiDisplayHint {
234
- type: 'value' | 'value+timestamp' | 'value+spark8w' | 'value+delta';
235
- icon?: string;
236
- }
237
- interface TenantColumnDef {
238
- key: string;
239
- label: string;
240
- /** Required path: /api/v1/admin/extras/...; MUST be batch-capable, no {slug}/{tenantId}. */
241
- endpoint: string;
242
- requiredCapability?: CapabilityKey;
243
- }
244
- interface TenantActionDef {
245
- /** `<projectKey>.<area>.<verb>`, e.g. `demoapp.datev.runExport`. */
246
- id: string;
247
- label: string;
248
- /** Lookup in the static actions: map of the shell build. */
249
- actionKey: ActionKey;
250
- requiredCapability?: CapabilityKey;
251
- requiresMfa?: boolean;
252
- confirmType?: 'none' | 'simple' | 'typed-slug' | 'typed-production' | 'date';
253
- }
254
- interface AuditActionDef {
255
- /** SCREAMING_SNAKE_CASE; matched to the AuditLog.action column. */
256
- key: string;
257
- label: string;
258
- severity?: 'info' | 'low' | 'medium' | 'high';
259
- }
260
- interface ManifestContribution {
261
- capabilities?: Record<CapabilityKey, boolean>;
262
- navigation?: {
263
- standardPages?: Partial<Record<StandardPageKey, StandardPageDef>>;
264
- projectPages?: ProjectPageDef[];
265
- };
266
- dashboard?: {
267
- kpiCards?: KpiCardDef[];
268
- };
269
- tenants?: {
270
- columns?: TenantColumnDef[];
271
- actions?: TenantActionDef[];
272
- };
273
- audit?: {
274
- actions?: AuditActionDef[];
275
- };
276
- }
277
- interface PublicBootResponse {
278
- project: {
279
- key: string;
280
- displayName: string;
281
- /** Tag/subtitle (e.g. "SuperAdmin"). From `saas.yaml#app.label`. */
282
- label?: string;
283
- /** Short abbreviation for the logo badge (e.g. "ma", "da"). From `saas.yaml#app.icon`. */
284
- icon?: string;
285
- logoUrl?: string;
286
- environment?: 'production' | 'staging' | 'development';
287
- };
288
- }
289
-
290
- /** Format: 'web:<email>:<sessionId>' or 'cli:<email>:<host>'. */
291
- type ActorTag = string;
292
- interface AuditEntry {
293
- id: string;
294
- /** null = platform action without tenant context (SUPER_ADMIN). */
295
- tenantId: string | null;
296
- /** null = system / cron-triggered. */
297
- userId: string | null;
298
- /** Convenience field; backend resolves it from userId. */
299
- userEmail: string | null;
300
- /** e.g. 'Tenant', 'PromoCode', 'Subscription', 'PlanVersion', 'User'. */
301
- entity: string;
302
- entityId: string;
303
- /** SCREAMING_SNAKE_CASE; past-tense oriented. */
304
- action: string;
305
- /** Freely structured. Convention: { field: { old, new } } or { reason, ... }. */
306
- changes: Record<string, unknown> | null;
307
- actorTag: ActorTag | null;
308
- ipAddress: string | null;
309
- userAgent: string | null;
310
- createdAt: string;
311
- }
312
- interface AuditQuery {
313
- tenantId?: string;
314
- userId?: string;
315
- entity?: string;
316
- entityId?: string;
317
- action?: string;
318
- /** Wildcard-capable, e.g. 'cli:*'. */
319
- actorTag?: string;
320
- from?: string;
321
- to?: string;
322
- page?: number;
323
- pageSize?: number;
324
- }
325
-
326
88
  /**
327
89
  * Approval lifecycle of a feature or a quota. Approval happens per
328
90
  * FEATURE/QUOTA — not per capability (#20); only `approved` entries
@@ -385,7 +147,6 @@ type CatalogEntryI18n = Record<string, CatalogEntryI18nFields>;
385
147
  */
386
148
  interface CapabilityCatalogEntryRow {
387
149
  id: string;
388
- projectKey: string;
389
150
  capabilityKey: string;
390
151
  label: string;
391
152
  description: string | null;
@@ -422,7 +183,6 @@ type FeatureTier = 'CORE' | 'ADVANCED' | 'PRO' | 'ENTERPRISE' | string;
422
183
  */
423
184
  interface FeatureCatalogEntryRow {
424
185
  id: string;
425
- projectKey: string;
426
186
  featureKey: string;
427
187
  label: string;
428
188
  description: string | null;
@@ -477,7 +237,6 @@ interface FeatureCatalogEntryRow {
477
237
  */
478
238
  interface QuotaCatalogEntryRow {
479
239
  id: string;
480
- projectKey: string;
481
240
  quotaKey: string;
482
241
  label: string;
483
242
  description: string | null;
@@ -521,7 +280,6 @@ interface QuotaCatalogEntryRow {
521
280
  * matching field is relevant.
522
281
  */
523
282
  interface CatalogEntryFilter {
524
- projectKey: string;
525
283
  discoveryStatus?: DiscoveryStatus;
526
284
  codeStatus?: CapabilityCodeStatus;
527
285
  }
@@ -613,6 +371,18 @@ interface MarketingTopFeature {
613
371
  label: string;
614
372
  strong: string;
615
373
  }
374
+ /**
375
+ * The range a marketing projection's `priority` may take.
376
+ *
377
+ * Declared here rather than in the DTO because two sides need the same answer:
378
+ * the request pipe rejects anything outside it, and the admin UI computes
379
+ * priorities when an operator drags a plan into a new position. A UI that
380
+ * picked its own bounds would produce a value the pipe refuses — and it did,
381
+ * at the top of the range, where a list of tied plans was lifted past the
382
+ * maximum to pull the ties apart.
383
+ */
384
+ declare const MARKETING_PRIORITY_MIN = 0;
385
+ declare const MARKETING_PRIORITY_MAX = 10000;
616
386
  /**
617
387
  * Locale-specific marketing texts per plan/bundle version.
618
388
  * Read and projected by the Public-Catalog-Controller
@@ -623,7 +393,6 @@ interface MarketingTopFeature {
623
393
  */
624
394
  interface MarketingProjectionRow {
625
395
  id: string;
626
- projectKey: string;
627
396
  targetType: MarketingTargetType;
628
397
  targetVersionId: string;
629
398
  /** ISO-639-1, optionally with region suffix (`de`, `en`, `de-AT`). */
@@ -668,42 +437,178 @@ interface MarketingProjectionRow {
668
437
  createdAt: string;
669
438
  updatedAt: string;
670
439
  }
671
- /** Filter for `MarketingProjectionRepository.list()`. At least projectKey. */
440
+ /** Filter for `MarketingProjectionRepository.list()`. */
672
441
  interface MarketingProjectionFilter {
673
- projectKey: string;
674
442
  targetType?: MarketingTargetType;
675
443
  targetVersionId?: string;
676
444
  locale?: string;
677
445
  }
678
- interface CreateMarketingProjectionData {
679
- projectKey: string;
680
- targetType: MarketingTargetType;
681
- targetVersionId: string;
682
- locale?: string;
683
- displayLabel: string;
684
- description: string;
685
- visible?: boolean;
686
- badge?: string;
687
- topFeatures?: MarketingTopFeature[];
688
- trialEnabled?: boolean;
689
- trialDays?: number;
690
- priceTag?: string | null;
691
- ctaLabel?: string | null;
692
- priority?: number;
693
- highlight?: boolean;
446
+ interface CreateMarketingProjectionData {
447
+ targetType: MarketingTargetType;
448
+ targetVersionId: string;
449
+ locale?: string;
450
+ displayLabel: string;
451
+ description: string;
452
+ visible?: boolean;
453
+ badge?: string;
454
+ topFeatures?: MarketingTopFeature[];
455
+ trialEnabled?: boolean;
456
+ trialDays?: number;
457
+ priceTag?: string | null;
458
+ ctaLabel?: string | null;
459
+ priority?: number;
460
+ highlight?: boolean;
461
+ }
462
+ interface UpdateMarketingProjectionData {
463
+ displayLabel?: string;
464
+ description?: string;
465
+ visible?: boolean;
466
+ badge?: string;
467
+ topFeatures?: MarketingTopFeature[];
468
+ trialEnabled?: boolean;
469
+ trialDays?: number;
470
+ priceTag?: string | null;
471
+ ctaLabel?: string | null;
472
+ priority?: number;
473
+ highlight?: boolean;
474
+ }
475
+
476
+ type FeatureKey = string;
477
+ type PlanId = string;
478
+ type QuotaKey = string;
479
+ interface FeatureDef {
480
+ key: FeatureKey;
481
+ label?: string;
482
+ icon?: string;
483
+ /** CORE / ADVANCED / PRO / BUSINESS / ENTERPRISE_ONLY — convention. */
484
+ tier?: string;
485
+ plannedOnly?: boolean;
486
+ }
487
+ interface PlanDef {
488
+ id: PlanId;
489
+ name?: string;
490
+ tagline?: string;
491
+ /** false = not selectable in self-service onboarding. Default: true. */
492
+ marketed?: boolean;
493
+ /** Highlighted card in onboarding (max. 1 per catalog). */
494
+ popular?: boolean;
495
+ /** Net monthly price. null = on request. */
496
+ monthlyNet?: number | null;
497
+ /** Net total amount per year. null = monthly only. */
498
+ yearlyNet?: number | null;
499
+ /** Map quotaKey → max value. -1 = unlimited. */
500
+ quotas: Record<QuotaKey, number>;
501
+ features: FeatureKey[];
502
+ }
503
+ /** App-wide marketing configuration. */
504
+ interface PlanCatalogMarketing {
505
+ /**
506
+ * Allowed language pool that the app may market. First = default
507
+ * locale. From it, the SuperAdmin activates a subset in the marketing
508
+ * catalog (LocaleManager).
509
+ */
510
+ availableLocales: string[];
511
+ }
512
+ /**
513
+ * App identity block for branding + version. Consumed by the `AdminPublicBootController`
514
+ * and the `AdminManifestConfigFactory`; the SuperAdmin UI (platform
515
+ * LoginPage, AdminLayout brand block) reads the same fields via PublicBoot.
516
+ *
517
+ * `name` = brand display name (e.g. "DemoApp", "ClubApp").
518
+ * `label` = tag/subtitle in the brand block (e.g. "SuperAdmin").
519
+ * `version` = app version string (build info).
520
+ * `icon` = 2-character abbreviation for the logo badge (e.g. "ma", "da").
521
+ * `logoUrl` = optional URL to a PNG/SVG; if set, the UI renders an <img>
522
+ * instead of the initials badge.
523
+ */
524
+ interface PlanCatalogApp {
525
+ name: string;
526
+ label?: string;
527
+ version?: string;
528
+ icon?: string;
529
+ logoUrl?: string;
530
+ }
531
+ /**
532
+ * Notice periods, one per rhythm.
533
+ *
534
+ * One number for both was the shape until 2026-08-27, and it could not be right
535
+ * for both: a yearly contract with a fortnight of notice is unusual, and a
536
+ * monthly contract with three months of notice is void against a consumer. The
537
+ * two are configured apart because real contracts set them apart.
538
+ *
539
+ * Both members are required. A missing rhythm would read as zero, and a silent
540
+ * zero is a commercial decision nobody made — the same defect one level below
541
+ * the one that moved these settings into the file.
542
+ *
543
+ * **No ceiling is enforced.** §309 Nr. 9 BGB limits the notice period in German
544
+ * consumer contracts to one month, and an installation serving businesses is
545
+ * not bound by it. The platform cannot know which it is, so the number is the
546
+ * consumer app's to choose and this is the sentence that says what it costs.
547
+ */
548
+ interface CancellationNoticePeriods {
549
+ /** Days of notice for a monthly subscription. */
550
+ monthly: number;
551
+ /** Days of notice for a yearly subscription. */
552
+ yearly: number;
553
+ }
554
+ /**
555
+ * Plans a tenant may not reach or leave without talking to sales.
556
+ *
557
+ * `asTarget`: may not be selected via self-service — typically ENTERPRISE,
558
+ * which only a special contract activates. `asSource`: may not be left via
559
+ * self-service — typically an active special contract.
560
+ *
561
+ * Both lists are required and may be empty. An empty list says out loud that
562
+ * self-service reaches every plan, which is a decision rather than an omission.
563
+ */
564
+ interface SelfServiceBlockedPlans {
565
+ asTarget: string[];
566
+ asSource: string[];
567
+ }
568
+ /**
569
+ * Commercial settings for the tenant-facing self-service routes.
570
+ *
571
+ * They live in `config/saas.yaml` and nowhere else: an operator reading the
572
+ * file has to be reading the values that are running, with no "unless somebody
573
+ * passed it in code" attached. The file is read at boot, so an edit lands on
574
+ * the next restart.
575
+ */
576
+ interface PlanCatalogTenantBilling {
577
+ cancellationNoticeDays: CancellationNoticePeriods;
578
+ selfServiceBlockedPlans: SelfServiceBlockedPlans;
579
+ }
580
+ /**
581
+ * Who is told when the settings in the file change between two starts.
582
+ *
583
+ * The record inside the application is written whether or not anybody is
584
+ * named here; mail is the addition, never the substitute. Mailed only where an
585
+ * email port is bound — without one the boot log says so once, and the change
586
+ * is recorded in the application only.
587
+ */
588
+ interface PlanCatalogNotifications {
589
+ /** Addresses mailed when a start finds the applied settings changed. */
590
+ settingsChanged?: string[];
694
591
  }
695
- interface UpdateMarketingProjectionData {
696
- displayLabel?: string;
697
- description?: string;
698
- visible?: boolean;
699
- badge?: string;
700
- topFeatures?: MarketingTopFeature[];
701
- trialEnabled?: boolean;
702
- trialDays?: number;
703
- priceTag?: string | null;
704
- ctaLabel?: string | null;
705
- priority?: number;
706
- highlight?: boolean;
592
+ interface PlanCatalog {
593
+ schemaVersion: 1;
594
+ /** App identity (branding + version), see PlanCatalogApp. */
595
+ app: PlanCatalogApp;
596
+ /** ISO-4217 currency code. */
597
+ currency: string;
598
+ /** VAT rate in percent. */
599
+ vatRate: number;
600
+ /** Commercial settings for the tenant self-service routes. */
601
+ tenantBilling: PlanCatalogTenantBilling;
602
+ /** App-wide marketing configuration. Optional. */
603
+ marketing?: PlanCatalogMarketing;
604
+ /** Who is told when the settings change between two starts. Optional. */
605
+ notifications?: PlanCatalogNotifications;
606
+ features?: FeatureDef[];
607
+ /**
608
+ * Optional. When omitted, plans come exclusively from the
609
+ * AdminUI / DB table (Plans/PlanVersions lifecycle).
610
+ */
611
+ plans?: PlanDef[];
707
612
  }
708
613
 
709
614
  type PromoCodeValueType = 'PERCENT' | 'ABSOLUTE';
@@ -902,7 +807,7 @@ interface VersionedEntityBase {
902
807
  * own migration).
903
808
  * - `startedAt` is the contract start of this booking.
904
809
  * - `minimumTermEndsAt` = end of the minimum term; `null` = no minimum term
905
- * (platform default = 12 months, set service-side).
810
+ * (platform default = no commitment, set service-side).
906
811
  * - `canceledAt` / `canceledEffectiveAt`: cancellation anchor vs. effective
907
812
  * date. Before the minimum term ends, `canceledEffectiveAt =
908
813
  * minimumTermEndsAt`, otherwise the subscription's period end.
@@ -919,6 +824,18 @@ interface SubscriptionBundleRecord {
919
824
  minimumTermEndsAt: Date | null;
920
825
  canceledAt: Date | null;
921
826
  canceledEffectiveAt: Date | null;
827
+ /**
828
+ * The rhythm this booking is billed in, and the window it is billed for.
829
+ *
830
+ * A bundle's periods end on the day its plan's do — the first one short,
831
+ * from the booking to the next occurrence of that day, and every one after
832
+ * it anchor to anchor. Null on a booking made before these fields existed,
833
+ * or on one whose plan has no period; readers fall back to the plan's
834
+ * cycle, which is what every booking used before.
835
+ */
836
+ billingCycle: string | null;
837
+ currentPeriodStart: Date | null;
838
+ currentPeriodEnd: Date | null;
922
839
  createdAt: Date;
923
840
  updatedAt: Date;
924
841
  }
@@ -932,14 +849,27 @@ interface SubscriptionBundleRecord {
932
849
  interface SubscriptionBundleView extends SubscriptionBundleRecord {
933
850
  bundleKey: string | null;
934
851
  label: string | null;
935
- monthlyNet: string | null;
852
+ /**
853
+ * What this booking is billed at, in the rhythm it was booked in and with
854
+ * the plan's pricing override applied.
855
+ *
856
+ * It was `monthlyNet` until 2026-08-27 and carried the bundle's base
857
+ * monthly price whatever the booking was — so a yearly booking of a bundle
858
+ * priced 10 monthly and 100 yearly reported 10. The name was half the
859
+ * defect: a field called `monthlyNet` on a yearly booking cannot be right.
860
+ */
861
+ priceNet: number | null;
936
862
  }
937
863
  interface CreateSubscriptionBundleData {
938
864
  subscriptionId: string;
939
865
  bundleVersionId: string;
940
866
  startedAt: Date;
941
- /** Default = startedAt + 12 months, unless set. */
867
+ /** Null unless a commitment was configured or asked for. */
942
868
  minimumTermEndsAt?: Date | null;
869
+ /** The rhythm and window worked out above this port. */
870
+ billingCycle?: string | null;
871
+ currentPeriodStart?: Date | null;
872
+ currentPeriodEnd?: Date | null;
943
873
  }
944
874
  interface CancelSubscriptionBundleData {
945
875
  canceledAt: Date;
@@ -988,7 +918,6 @@ interface BundlePricingOverride {
988
918
  */
989
919
  interface BundleRow {
990
920
  id: string;
991
- projectKey: string;
992
921
  bundleKey: string;
993
922
  label: string;
994
923
  description: string | null;
@@ -1027,7 +956,6 @@ interface BundleVersionRow extends VersionedEntityBase {
1027
956
  * first BundleVersion via `CreateBundleVersionDraftData`.
1028
957
  */
1029
958
  interface CreateBundleData {
1030
- projectKey: string;
1031
959
  bundleKey: string;
1032
960
  label: string;
1033
961
  description?: string | null;
@@ -1036,9 +964,9 @@ interface CreateBundleData {
1036
964
  i18n?: CatalogEntryI18n;
1037
965
  }
1038
966
  /**
1039
- * Fields that may be changed on the bundle master. `bundleKey` and
1040
- * `projectKey` are intentionally not here — master identity is immutable;
1041
- * whoever wants to change them creates a new bundle and retires the old one.
967
+ * Fields that may be changed on the bundle master. `bundleKey` is
968
+ * intentionally not here — master identity is immutable; whoever wants to
969
+ * change it creates a new bundle and retires the old one.
1042
970
  */
1043
971
  interface UpdateBundleData {
1044
972
  label?: string;
@@ -1161,16 +1089,273 @@ interface StrictModeWarning {
1161
1089
  /** The concrete violating value; optional. */
1162
1090
  value?: string;
1163
1091
  }
1164
- /**
1165
- * Service result for mutating Bundle operations
1166
- * (createDraft, updateDraft, publish): returns the persisted row plus
1167
- * a list of strict-mode warnings. In `warn-only` mode the
1168
- * warnings go into the UI as a banner; in `blocking` mode the service throws
1169
- * HTTP 422 instead, with the same warning list as the body.
1170
- */
1171
- interface BundleVersionMutationResult {
1172
- bundleVersion: BundleVersionRow;
1173
- warnings: StrictModeWarning[];
1092
+ /**
1093
+ * Service result for mutating Bundle operations
1094
+ * (createDraft, updateDraft, publish): returns the persisted row plus
1095
+ * a list of strict-mode warnings. In `warn-only` mode the
1096
+ * warnings go into the UI as a banner; in `blocking` mode the service throws
1097
+ * HTTP 422 instead, with the same warning list as the body.
1098
+ */
1099
+ interface BundleVersionMutationResult {
1100
+ bundleVersion: BundleVersionRow;
1101
+ warnings: StrictModeWarning[];
1102
+ }
1103
+
1104
+ /**
1105
+ * The column values a new BundleVersion draft starts from.
1106
+ *
1107
+ * Every adapter has to apply the same defaults — an absent quota map is `{}`,
1108
+ * an absent price is null rather than zero, an unstated `marketed` is true —
1109
+ * and two adapters spelling that out separately is the same decision written
1110
+ * twice. It is also the variant jscpd does catch, which is how this came out:
1111
+ * `adapter-drizzle` learning about bundles put a second copy beside
1112
+ * `adapter-prisma`'s.
1113
+ *
1114
+ * Validity windows are deliberately absent. Whether a draft carries
1115
+ * `validFrom`/`validUntil` is an adapter capability rather than a default, and
1116
+ * an adapter that does not maintain those columns must not write them.
1117
+ */
1118
+ declare function bundleDraftDefaults(data: CreateBundleVersionDraftData): {
1119
+ baseVersionId: string | null;
1120
+ features: string[];
1121
+ quotas: Record<string, number>;
1122
+ compatibility: Record<string, unknown>;
1123
+ pricingOverrides: unknown[];
1124
+ monthlyNet: string | null;
1125
+ yearlyNet: string | null;
1126
+ marketed: boolean;
1127
+ changeNote: string;
1128
+ createdByUserId: string | null;
1129
+ };
1130
+ /**
1131
+ * The column values a new Bundle stem starts from.
1132
+ *
1133
+ * The same defaulting rule as above, one level up: an absent description or
1134
+ * icon is null rather than an empty string, an unstated sort order is 0, an
1135
+ * absent translation map is `{}`. Written out in five places before this — two
1136
+ * adapters and two fakes — which is four opportunities for one of them to
1137
+ * decide differently.
1138
+ */
1139
+ declare function bundleStemDefaults(data: CreateBundleData): {
1140
+ bundleKey: string;
1141
+ label: string;
1142
+ description: string | null;
1143
+ icon: string | null;
1144
+ sortOrder: number;
1145
+ i18n: CatalogEntryI18n;
1146
+ };
1147
+ /** The stored shape both adapters read a bundle stem back from. */
1148
+ interface StoredBundleStem {
1149
+ id: string;
1150
+ bundleKey: string;
1151
+ label: string;
1152
+ description: string | null;
1153
+ icon: string | null;
1154
+ sortOrder: number;
1155
+ i18n: unknown;
1156
+ createdAt: Date;
1157
+ updatedAt: Date;
1158
+ deletedAt: Date | null;
1159
+ }
1160
+ /**
1161
+ * A stored bundle stem as the port describes it.
1162
+ *
1163
+ * The two stores spell the columns identically, so the mapping was identical
1164
+ * too — and an identical mapping in two files is one place for a field to be
1165
+ * forgotten when the row grows. `i18n` arrives as JSON of unknown shape from
1166
+ * both, and a non-object becomes `{}` rather than reaching a caller that
1167
+ * expects a map.
1168
+ */
1169
+ declare function toBundleStemRow(row: StoredBundleStem): BundleRow;
1170
+ /**
1171
+ * The fields a caller actually gave, as a patch.
1172
+ *
1173
+ * The update DTOs in this codebase mean three different things by three
1174
+ * different values: a value changes the column, an explicit `null` clears it,
1175
+ * and an **omitted** field leaves it alone. Only the last one needs care, and
1176
+ * it was written out as `...(data.x !== undefined ? { x: data.x } : {})` more
1177
+ * than fifty times across five repositories — one decision, fifty
1178
+ * opportunities to spell it differently, and the duplication ratchet is what
1179
+ * finally pointed at it.
1180
+ *
1181
+ * `null` is deliberately kept: it is a value a caller chose, not an absence.
1182
+ */
1183
+ declare function definedFields<T extends object, K extends keyof T>(data: T, keys: readonly K[]): Partial<Pick<T, K>>;
1184
+
1185
+ /** Backend capability key, convention: domain.action[.action]. */
1186
+ type CapabilityKey = string;
1187
+ /** Frontend action-registry key. Same convention as CapabilityKey. */
1188
+ type ActionKey = CapabilityKey;
1189
+ /** Lookup key in the static extensions: map of the UI build. */
1190
+ type ComponentKey = string;
1191
+ interface AdminManifest {
1192
+ schemaVersion: 1;
1193
+ project: {
1194
+ key: string;
1195
+ displayName: string;
1196
+ /** Tag/subtitle (e.g. "SuperAdmin"). From `saas.yaml#app.label`. */
1197
+ label?: string;
1198
+ /** Short abbreviation for the logo badge (e.g. "ma", "da"). From `saas.yaml#app.icon`. */
1199
+ icon?: string;
1200
+ logoUrl?: string;
1201
+ environment?: 'production' | 'staging' | 'development';
1202
+ /**
1203
+ * Allowed locale pool from the app config (`saas.yaml`
1204
+ * `marketing.availableLocales`). First = default..
1205
+ */
1206
+ availableLocales?: string[];
1207
+ /** Default locale; equals `availableLocales[0]`. */
1208
+ defaultLocale?: string;
1209
+ };
1210
+ build: {
1211
+ platformPackageVersion: string;
1212
+ appVersion: string;
1213
+ manifestHash: string;
1214
+ };
1215
+ planCatalogSnapshot: {
1216
+ source: string;
1217
+ hash: string;
1218
+ currency: string;
1219
+ vatRate: number;
1220
+ features?: FeatureDef[];
1221
+ plans: PlanDef[];
1222
+ };
1223
+ /** Map CapabilityKey → boolean. Manifest is never a security source. */
1224
+ capabilities: Record<CapabilityKey, boolean>;
1225
+ navigation: {
1226
+ standardPages: Partial<Record<StandardPageKey, StandardPageDef>>;
1227
+ projectPages?: ProjectPageDef[];
1228
+ };
1229
+ dashboard?: {
1230
+ kpiCards?: KpiCardDef[];
1231
+ };
1232
+ tenants?: {
1233
+ columns?: TenantColumnDef[];
1234
+ actions?: TenantActionDef[];
1235
+ };
1236
+ audit?: {
1237
+ actions?: AuditActionDef[];
1238
+ };
1239
+ }
1240
+ type StandardPageKey = 'dashboard' | 'tenants' | 'subscriptions' | 'promoCodes' | 'plans' | 'audit' | 'users' | 'pilots' | 'discovery' | 'bundles' | 'marketingCatalog' | 'platformEmail' | 'platformEmailHistory' | 'settings';
1241
+ interface StandardPageDef {
1242
+ enabled: boolean;
1243
+ requiredCapability?: CapabilityKey;
1244
+ }
1245
+ interface ProjectPageDef {
1246
+ /** `<app>.<area>`, e.g. `demoapp.datev`. */
1247
+ id: string;
1248
+ label: string;
1249
+ icon?: string;
1250
+ /** Frontend route, e.g. `/admin/datev`. */
1251
+ route: string;
1252
+ navSection?: string;
1253
+ /** Lookup in the static extensions: map of the shell build. */
1254
+ componentKey: ComponentKey;
1255
+ requiredCapability?: CapabilityKey;
1256
+ prefetchOnIdle?: boolean;
1257
+ }
1258
+ interface KpiCardDef {
1259
+ id: string;
1260
+ label: string;
1261
+ /** Required path: /api/v1/admin/(extras|dashboard)/... */
1262
+ endpoint: string;
1263
+ displayHint: KpiDisplayHint;
1264
+ /** 0–100; UI sorts descending. */
1265
+ slotPriority?: number;
1266
+ requiredCapability?: CapabilityKey;
1267
+ }
1268
+ interface KpiDisplayHint {
1269
+ type: 'value' | 'value+timestamp' | 'value+spark8w' | 'value+delta';
1270
+ icon?: string;
1271
+ }
1272
+ interface TenantColumnDef {
1273
+ key: string;
1274
+ label: string;
1275
+ /** Required path: /api/v1/admin/extras/...; MUST be batch-capable, no {slug}/{tenantId}. */
1276
+ endpoint: string;
1277
+ requiredCapability?: CapabilityKey;
1278
+ }
1279
+ interface TenantActionDef {
1280
+ /** `<app>.<area>.<verb>`, e.g. `demoapp.datev.runExport`. */
1281
+ id: string;
1282
+ label: string;
1283
+ /** Lookup in the static actions: map of the shell build. */
1284
+ actionKey: ActionKey;
1285
+ requiredCapability?: CapabilityKey;
1286
+ requiresMfa?: boolean;
1287
+ confirmType?: 'none' | 'simple' | 'typed-slug' | 'typed-production' | 'date';
1288
+ }
1289
+ interface AuditActionDef {
1290
+ /** SCREAMING_SNAKE_CASE; matched to the AuditLog.action column. */
1291
+ key: string;
1292
+ label: string;
1293
+ severity?: 'info' | 'low' | 'medium' | 'high';
1294
+ }
1295
+ interface ManifestContribution {
1296
+ capabilities?: Record<CapabilityKey, boolean>;
1297
+ navigation?: {
1298
+ standardPages?: Partial<Record<StandardPageKey, StandardPageDef>>;
1299
+ projectPages?: ProjectPageDef[];
1300
+ };
1301
+ dashboard?: {
1302
+ kpiCards?: KpiCardDef[];
1303
+ };
1304
+ tenants?: {
1305
+ columns?: TenantColumnDef[];
1306
+ actions?: TenantActionDef[];
1307
+ };
1308
+ audit?: {
1309
+ actions?: AuditActionDef[];
1310
+ };
1311
+ }
1312
+ interface PublicBootResponse {
1313
+ project: {
1314
+ key: string;
1315
+ displayName: string;
1316
+ /** Tag/subtitle (e.g. "SuperAdmin"). From `saas.yaml#app.label`. */
1317
+ label?: string;
1318
+ /** Short abbreviation for the logo badge (e.g. "ma", "da"). From `saas.yaml#app.icon`. */
1319
+ icon?: string;
1320
+ logoUrl?: string;
1321
+ environment?: 'production' | 'staging' | 'development';
1322
+ };
1323
+ }
1324
+
1325
+ /** Format: 'web:<email>:<sessionId>' or 'cli:<email>:<host>'. */
1326
+ type ActorTag = string;
1327
+ interface AuditEntry {
1328
+ id: string;
1329
+ /** null = platform action without tenant context (SUPER_ADMIN). */
1330
+ tenantId: string | null;
1331
+ /** null = system / cron-triggered. */
1332
+ userId: string | null;
1333
+ /** Convenience field; backend resolves it from userId. */
1334
+ userEmail: string | null;
1335
+ /** e.g. 'Tenant', 'PromoCode', 'Subscription', 'PlanVersion', 'User'. */
1336
+ entity: string;
1337
+ entityId: string;
1338
+ /** SCREAMING_SNAKE_CASE; past-tense oriented. */
1339
+ action: string;
1340
+ /** Freely structured. Convention: { field: { old, new } } or { reason, ... }. */
1341
+ changes: Record<string, unknown> | null;
1342
+ actorTag: ActorTag | null;
1343
+ ipAddress: string | null;
1344
+ userAgent: string | null;
1345
+ createdAt: string;
1346
+ }
1347
+ interface AuditQuery {
1348
+ tenantId?: string;
1349
+ userId?: string;
1350
+ entity?: string;
1351
+ entityId?: string;
1352
+ action?: string;
1353
+ /** Wildcard-capable, e.g. 'cli:*'. */
1354
+ actorTag?: string;
1355
+ from?: string;
1356
+ to?: string;
1357
+ page?: number;
1358
+ pageSize?: number;
1174
1359
  }
1175
1360
 
1176
1361
  /** Promotion type. */
@@ -1201,7 +1386,6 @@ type PromotionI18n = Record<string, PromotionI18nFields>;
1201
1386
  /** Wire format of a `promotions` row. */
1202
1387
  interface PromotionRow {
1203
1388
  id: string;
1204
- projectKey: string;
1205
1389
  /** Internal label (not public). */
1206
1390
  internalLabel: string;
1207
1391
  type: PromotionType;
@@ -1227,11 +1411,7 @@ interface PromotionRow {
1227
1411
  createdAt: string;
1228
1412
  updatedAt: string;
1229
1413
  }
1230
- interface PromotionFilter {
1231
- projectKey: string;
1232
- }
1233
1414
  interface CreatePromotionData {
1234
- projectKey: string;
1235
1415
  internalLabel: string;
1236
1416
  type: PromotionType;
1237
1417
  value: PromotionValue;
@@ -1354,7 +1534,6 @@ type CheckoutOfferStatus = 'open' | 'consumed' | 'expired';
1354
1534
  /** Wire format of a `checkout_offers` row. */
1355
1535
  interface CheckoutOfferRow {
1356
1536
  id: string;
1357
- projectKey: string;
1358
1537
  /** Plan selected on the website. */
1359
1538
  planKey: string;
1360
1539
  /** Resolved plan version, if known. */
@@ -1385,12 +1564,10 @@ interface CheckoutOfferRow {
1385
1564
  updatedAt: string;
1386
1565
  }
1387
1566
  interface CheckoutOfferFilter {
1388
- projectKey: string;
1389
1567
  status?: CheckoutOfferStatus;
1390
1568
  }
1391
1569
  /** Body of `POST /public/checkout-offer` — called from the website. */
1392
1570
  interface CreateCheckoutOfferData {
1393
- projectKey: string;
1394
1571
  planKey: string;
1395
1572
  planVersionId?: string | null;
1396
1573
  billingCycle: 'monthly' | 'yearly';
@@ -1426,7 +1603,6 @@ interface UpdateCheckoutOfferData {
1426
1603
 
1427
1604
  /** Wire format of the `marketing_settings` row. */
1428
1605
  interface MarketingSettingsRow {
1429
- projectKey: string;
1430
1606
  /** Runtime-activated subset of the `availableLocales` pool. */
1431
1607
  activeLocales: string[];
1432
1608
  updatedAt: string;
@@ -1462,6 +1638,10 @@ interface PublicMarketingPlan {
1462
1638
  badge: string;
1463
1639
  /** Teaser / description text. */
1464
1640
  description: string;
1641
+ /**
1642
+ * The recommended plan, and at most one card in a catalogue carries it —
1643
+ * see `keepOneRecommended`, which decides it per language served.
1644
+ */
1465
1645
  highlight: boolean;
1466
1646
  /**
1467
1647
  * Formatted pricing tag from the MarketingProjection (#47, e.g.
@@ -1544,7 +1724,6 @@ interface PublicComparisonRow {
1544
1724
  }
1545
1725
  /** Response of `GET /public/marketing-catalog`. */
1546
1726
  interface PublicMarketingCatalogResponse {
1547
- projectKey: string;
1548
1727
  locale: string;
1549
1728
  currency: string;
1550
1729
  /** VAT rate in percent — for the CheckoutOffer price breakdown. */
@@ -1654,7 +1833,7 @@ interface DiscoverySnapshot {
1654
1833
  /** ISO timestamp of the boot-time scan. */
1655
1834
  scannedAt: string;
1656
1835
  app: {
1657
- /** projectKey, same concept as in the catalog tables. */
1836
+ /** The application's name, from `saas.yaml#app.name`. */
1658
1837
  key: string;
1659
1838
  /** Backend version, e.g. from package.json. */
1660
1839
  version: string;
@@ -1690,6 +1869,18 @@ interface FeatureUiMeta {
1690
1869
  /** Map FeatureKey → UI metadata. Consumer apps supply a complete table. */
1691
1870
  type FeatureUiRegistry = Record<string, FeatureUiMeta>;
1692
1871
 
1872
+ /** A quota as a finite number, or `null` where the value cannot be read as one. */
1873
+ declare function readQuotaValue(value: unknown): number | null;
1874
+ /**
1875
+ * Every quota in a JSON column, for a caller that computes with them.
1876
+ *
1877
+ * A key that is there stays there. Dropping an unreadable one made it *absent*,
1878
+ * and absent means undeclared: `enforceLimit` answers an undeclared dimension
1879
+ * with a 500, so every operation on that quota was refused — a fail-closed
1880
+ * answer to somebody else's corrupt row.
1881
+ */
1882
+ declare function readQuotaRecord(value: unknown): Record<string, number>;
1883
+
1693
1884
  /** Alias for historical compatibility — equivalent to VersionChangeDirection. */
1694
1885
  type ChangeDirection = VersionChangeDirection;
1695
1886
  interface DiffResult {
@@ -1707,9 +1898,16 @@ type DecimalLike = number | string | {
1707
1898
  };
1708
1899
  interface PlanVersionFields {
1709
1900
  features: FeatureKey[];
1710
- maxUsers: number;
1711
- maxVehicles: number;
1712
- maxStorageGb: number;
1901
+ /**
1902
+ * Quotas of the version. -1 = unlimited; missing key = 0.
1903
+ *
1904
+ * Every key either side carries is compared. Which keys exist is the
1905
+ * installation's decision — they come from `@DefinesQuota` — so a fixed
1906
+ * set here would have compared the three the platform happened to know by
1907
+ * name and let every other one be lowered without the confirmation
1908
+ * publishing a regression asks for.
1909
+ */
1910
+ quotas: Record<QuotaKey, number>;
1713
1911
  monthlyNet: DecimalLike;
1714
1912
  yearlyNet: DecimalLike;
1715
1913
  }
@@ -1725,8 +1923,7 @@ declare function classifyPlanDiff(oldV: PlanVersionFields, newV: PlanVersionFiel
1725
1923
  /**
1726
1924
  * Classification of a BundleVersion diff for contract protection.
1727
1925
  *
1728
- * Quota comparison: `-1` (unlimited) is always better than any positive
1729
- * number. Otherwise higher = better. Missing keys are treated as 0.
1926
+ * Quotas are compared exactly as they are for a plan.
1730
1927
  *
1731
1928
  * Pricing can be `null` (the bundle only has override pricing); a switch
1732
1929
  * from value ↔ null is classified as REGRESSION (value dropped) or IMPROVEMENT
@@ -1824,9 +2021,86 @@ interface OnboardingPromoRedemption {
1824
2021
  endsAt: string | null;
1825
2022
  }
1826
2023
 
2024
+ /**
2025
+ * The settings subtree of a plan catalogue: every top-level block that is
2026
+ * configuration rather than the catalogue itself. JSON-shaped, because it is
2027
+ * stored as JSON and compared as JSON.
2028
+ */
2029
+ type AppliedSettingsValues = Record<string, unknown>;
2030
+ /** The one row per installation: what is applied, since when, and from where. */
2031
+ interface AppliedSettingsRecord {
2032
+ /**
2033
+ * `sha256-<hex>` over the canonical JSON of `settings`. Two boots with the
2034
+ * same resolved values produce the same fingerprint however the file was
2035
+ * formatted, and a plan added to the catalogue does not move it.
2036
+ */
2037
+ fingerprint: string;
2038
+ settings: AppliedSettingsValues;
2039
+ /**
2040
+ * Where the values came from: the absolute path of the file the platform
2041
+ * read, or a phrase saying they were handed to it in code.
2042
+ */
2043
+ source: string;
2044
+ /** The moment these values became the running configuration. */
2045
+ appliedAt: Date;
2046
+ }
2047
+ /** What a boot noticed had changed since the previous record. */
2048
+ interface SettingsChangeRecord {
2049
+ id: string;
2050
+ /** The boot that noticed the difference and applied the new values. */
2051
+ noticedAt: Date;
2052
+ source: string;
2053
+ previous: AppliedSettingsValues;
2054
+ current: AppliedSettingsValues;
2055
+ /** Set once an operator has seen it; null while it is still owed a look. */
2056
+ acknowledgedAt: Date | null;
2057
+ /** Who acknowledged it — an actor tag, as the audit log writes it. */
2058
+ acknowledgedBy: string | null;
2059
+ }
2060
+ type NewSettingsChange = Pick<SettingsChangeRecord, 'noticedAt' | 'source' | 'previous' | 'current'>;
2061
+ /** One leaf that differs between two settings subtrees. */
2062
+ interface SettingsDifference {
2063
+ /** Dotted path, as the loader names a field: `tenantBilling.cancellationNoticeDays.monthly`. */
2064
+ path: string;
2065
+ /** `undefined` where the leaf did not exist on that side. */
2066
+ before: unknown;
2067
+ after: unknown;
2068
+ }
2069
+
2070
+ /**
2071
+ * The top-level blocks of `config/saas.yaml` that are the catalogue rather than
2072
+ * the configuration, and the format marker.
2073
+ *
2074
+ * An exclusion list rather than a list of settings, on purpose: a block the
2075
+ * schema gains tomorrow is a setting until somebody says otherwise, so it is
2076
+ * fingerprinted by default. The failure mode of the other list — a new setting
2077
+ * silently left out of the fingerprint, so a change to it is never noticed — is
2078
+ * the one this record exists to prevent. `schemaVersion` is excluded because a
2079
+ * format change is a migration of the file, not a decision an operator took.
2080
+ *
2081
+ * `tests/settings-subtree.test.js` holds this list to the schema in both
2082
+ * directions: every name here is a property the schema declares, and every
2083
+ * property the schema declares lands on one side.
2084
+ */
2085
+ declare const CATALOGUE_KEYS: ReadonlySet<keyof PlanCatalog>;
2086
+ /** Everything in the catalogue that is configuration, as it was resolved. */
2087
+ declare function settingsSubtreeOf(catalog: PlanCatalog): AppliedSettingsValues;
2088
+ /**
2089
+ * `JSON.stringify` with object keys in sorted order at every depth, so that two
2090
+ * documents saying the same thing in a different order serialise identically.
2091
+ * Array order is kept: a list is what its author wrote, in the order they wrote
2092
+ * it.
2093
+ */
2094
+ declare function canonicalJson(value: unknown): string;
2095
+ /**
2096
+ * Every leaf that differs between `before` and `after`, in the order the paths
2097
+ * sort. A list counts as one leaf: `asTarget: [] → [ENTERPRISE]` is one thing
2098
+ * that changed, not a change per element.
2099
+ */
2100
+ declare function diffSettings(before: AppliedSettingsValues, after: AppliedSettingsValues): SettingsDifference[];
2101
+
1827
2102
  interface PlanRow {
1828
2103
  id: string;
1829
- projectKey: string;
1830
2104
  planKey: string;
1831
2105
  label: string;
1832
2106
  description: string | null;
@@ -1843,7 +2117,6 @@ interface PlanRow {
1843
2117
  * creation (follows in M6 Pack 2).
1844
2118
  */
1845
2119
  interface CreatePlanData {
1846
- projectKey: string;
1847
2120
  planKey: string;
1848
2121
  label: string;
1849
2122
  description?: string | null;
@@ -1851,9 +2124,9 @@ interface CreatePlanData {
1851
2124
  sortOrder?: number;
1852
2125
  }
1853
2126
  /**
1854
- * Fields that may be changed on the plan stem. `planKey` and `projectKey`
1855
- * are deliberately not here — stem identity is immutable; whoever wants to
1856
- * change it creates a new plan and retires the old one.
2127
+ * Fields that may be changed on the plan stem. `planKey` is deliberately not
2128
+ * here — stem identity is immutable; whoever wants to change it creates a new
2129
+ * plan and retires the old one.
1857
2130
  */
1858
2131
  interface UpdatePlanData {
1859
2132
  label?: string;
@@ -1917,7 +2190,6 @@ interface UpsertResult {
1917
2190
  skipReason?: string;
1918
2191
  }
1919
2192
  interface UpsertPlanInput {
1920
- projectKey: string;
1921
2193
  planKey: string;
1922
2194
  label: string;
1923
2195
  description?: string | null;
@@ -1937,7 +2209,6 @@ interface UpsertPlanVersionInput {
1937
2209
  changeNote: string;
1938
2210
  }
1939
2211
  interface UpsertFeatureCatalogEntryInput {
1940
- projectKey: string;
1941
2212
  featureKey: FeatureKey;
1942
2213
  label?: string;
1943
2214
  icon?: string;
@@ -1983,7 +2254,7 @@ interface PlanCatalogReadSnapshot {
1983
2254
  * implement it against their Prisma tables.
1984
2255
  */
1985
2256
  interface PlanCatalogReadSink {
1986
- loadSnapshot(projectKey: string): Promise<PlanCatalogReadSnapshot>;
2257
+ loadSnapshot(): Promise<PlanCatalogReadSnapshot>;
1987
2258
  }
1988
2259
 
1989
2260
  /**
@@ -2220,6 +2491,26 @@ interface PasswordHasher {
2220
2491
  hash(plain: string): Promise<string>;
2221
2492
  verify(hash: string, plain: string): Promise<boolean>;
2222
2493
  }
2494
+ /**
2495
+ * Sends a plain-text mail to an operator.
2496
+ *
2497
+ * The platform composes the text; the adapter delivers it — over whatever the
2498
+ * installation already sends mail with. Deliberately narrow: no templates, no
2499
+ * locale, no HTML. The one thing the platform mails today is a diagnostic for
2500
+ * the operator who runs the installation, and a diagnostic is English and
2501
+ * plain, like the boot log it mirrors. Tenant-facing mail — a verification
2502
+ * code, a resume link — goes through the registration module's own delivery
2503
+ * ports, which carry the locale and the person's name because that mail is
2504
+ * for a customer.
2505
+ */
2506
+ interface EmailPort {
2507
+ /** Delivers one plain-text mail to one address; rejects when it cannot. */
2508
+ send(message: {
2509
+ to: string;
2510
+ subject: string;
2511
+ text: string;
2512
+ }): Promise<void>;
2513
+ }
2223
2514
  /** Adapter for MFA secret persistence. */
2224
2515
  interface MfaPort {
2225
2516
  /** Returns the stored TOTP secret or null. */
@@ -2427,6 +2718,26 @@ interface PromoRevenueDeductionAggregator {
2427
2718
 
2428
2719
  type ContractLineItemKind = 'plan' | 'bundle' | 'discount';
2429
2720
  type SubscriptionContractStatus = 'active' | 'scheduled' | 'terminated' | 'superseded';
2721
+ /**
2722
+ * The statuses a contract is looked up under when asking "what is this tenant
2723
+ * on right now" — `scheduled` included, because a contract that starts today
2724
+ * and has not been switched to `active` yet is still the one in force at its
2725
+ * own `effectiveFrom`.
2726
+ *
2727
+ * One list rather than one per adapter: the two adapters have to answer
2728
+ * `findActiveByTenantId` the same way, and a status added here must not reach
2729
+ * only whichever of them somebody remembered.
2730
+ */
2731
+ /**
2732
+ * How many bundle versions one price lookup may name.
2733
+ *
2734
+ * One number rather than two: the server validates against it and the client
2735
+ * batches to stay inside it, and a client that learned the cap by receiving a
2736
+ * 400 would fail silently — the lookup answers with an empty map, and every
2737
+ * card falls back to a catalogue price the tenant may not be charged.
2738
+ */
2739
+ declare const BUNDLE_PRICE_LOOKUP_LIMIT = 200;
2740
+ declare const ACTIVE_SUBSCRIPTION_CONTRACT_STATUSES: readonly SubscriptionContractStatus[];
2430
2741
  interface ContractLineItemRecord {
2431
2742
  id: string;
2432
2743
  contractId: string;
@@ -2440,6 +2751,26 @@ interface ContractLineItemRecord {
2440
2751
  priceNet: number;
2441
2752
  priceGross: number;
2442
2753
  billingCycle: 'monthly' | 'yearly';
2754
+ /**
2755
+ * ISO 4217, as the line was booked in.
2756
+ *
2757
+ * An installation sells in one currency at a time, so this is never a
2758
+ * choice the line makes — it is what keeps the line meaning what it meant
2759
+ * after the configured currency is migrated to another one.
2760
+ */
2761
+ currency: string;
2762
+ /**
2763
+ * The tax rate in percent that was applied, recorded rather than left in
2764
+ * the ratio between net and gross. That ratio is not the rate: it cannot be
2765
+ * reproduced for a rounded gross, cannot express an exempt or reverse-charge
2766
+ * line, and does not survive a rate change.
2767
+ */
2768
+ taxRate: number;
2769
+ /**
2770
+ * The tax contained in the line — exactly `priceGross - priceNet`, so the
2771
+ * line cannot disagree with itself. Rounded once, when the line is written.
2772
+ */
2773
+ taxAmount: number;
2443
2774
  minimumTermUntil: Date | null;
2444
2775
  featuresSnapshot: string[];
2445
2776
  quotaEffectsSnapshot: Record<string, number>;
@@ -2452,12 +2783,23 @@ interface SubscriptionContractPriceSnapshot {
2452
2783
  subtotalNet: number;
2453
2784
  discountNet: number;
2454
2785
  totalNet: number;
2786
+ /**
2787
+ * The rate this contract's total was computed at — in per cent where the
2788
+ * contract was frozen from the catalogue, and as the offer stated it where
2789
+ * it was concluded from one.
2790
+ *
2791
+ * A checkout offer prices its lines as `net * (1 + vatRate)`, so it states
2792
+ * a fraction, and the value is copied here as it stands. The field
2793
+ * therefore carries both units across a history and cannot be compared
2794
+ * across contracts. `ContractLineItemRecord.taxRate` is always per cent and
2795
+ * is the one to read; this is kept as written because it is the record of
2796
+ * what the contract was concluded with.
2797
+ */
2455
2798
  vatRate: number;
2456
2799
  totalGross: number;
2457
2800
  }
2458
2801
  interface SubscriptionContractRecord {
2459
2802
  id: string;
2460
- projectKey: string;
2461
2803
  tenantId: string;
2462
2804
  status: SubscriptionContractStatus;
2463
2805
  effectiveFrom: Date;
@@ -2476,7 +2818,6 @@ interface SubscriptionContractRecord {
2476
2818
  }
2477
2819
  type NewContractLineItemData = Omit<ContractLineItemRecord, 'id' | 'contractId' | 'createdAt'>;
2478
2820
  interface CreateSubscriptionContractData {
2479
- projectKey: string;
2480
2821
  tenantId: string;
2481
2822
  status?: SubscriptionContractStatus;
2482
2823
  effectiveFrom: Date;
@@ -2493,10 +2834,22 @@ interface CreateSubscriptionContractData {
2493
2834
  }
2494
2835
  interface TerminateSubscriptionContractData {
2495
2836
  effectiveUntil: Date;
2496
- status: Extract<SubscriptionContractStatus, 'terminated' | 'superseded'>;
2837
+ /**
2838
+ * The terminal status, or `null` to end the contract by date alone.
2839
+ *
2840
+ * `findActiveByTenantId` already asks its question as a window —
2841
+ * `effectiveFrom <= asOf` and `effectiveUntil` null or after it — so a
2842
+ * contract given an end in the FUTURE is found until that moment and not
2843
+ * afterwards, with no scheduled job to flip anything.
2844
+ *
2845
+ * Writing a terminal status instead makes the contract disappear from that
2846
+ * lookup at once, which for a cancellation declared months ahead removes an
2847
+ * agreement the customer is still under. Null is how a caller says "it ends
2848
+ * then", and a status is how it says "it is over now".
2849
+ */
2850
+ status: Extract<SubscriptionContractStatus, 'terminated' | 'superseded'> | null;
2497
2851
  }
2498
2852
  interface SubscriptionContractFilter {
2499
- projectKey?: string;
2500
2853
  tenantId?: string;
2501
2854
  status?: SubscriptionContractStatus;
2502
2855
  asOf?: Date;
@@ -2513,12 +2866,14 @@ interface InvoiceLineItemSnapshot {
2513
2866
  priceNet: number;
2514
2867
  priceGross: number;
2515
2868
  billingCycle: 'monthly' | 'yearly';
2869
+ currency: string;
2870
+ taxRate: number;
2871
+ taxAmount: number;
2516
2872
  minimumTermUntil: Date | null;
2517
2873
  metadata: Record<string, unknown> | null;
2518
2874
  }
2519
2875
  interface SubscriptionContractInvoiceSnapshot {
2520
2876
  contractId: string;
2521
- projectKey: string;
2522
2877
  tenantId: string;
2523
2878
  originalOfferId: string | null;
2524
2879
  currency: string;
@@ -2552,6 +2907,24 @@ interface SubscriptionRecord {
2552
2907
  } | null;
2553
2908
  planVersionId: string;
2554
2909
  planVersion: PlanVersionRecord;
2910
+ /**
2911
+ * When a cancellation was declared, and when it takes effect.
2912
+ *
2913
+ * Required, and required together, because entitlement resolution ends a
2914
+ * subscription by reading them: without the second date it cannot tell a
2915
+ * subscription that ends next January from one that ended last January, and
2916
+ * it grants the latter everything. Nothing else in the platform would
2917
+ * notice — no repository filters a cancelled subscription out, and stopping
2918
+ * the billing period is a different decision from ending what a tenant may
2919
+ * do.
2920
+ *
2921
+ * `null` on both means no cancellation. On a row written before the two
2922
+ * fields separated, `canceledAt` carries the effective date and
2923
+ * `canceledEffectiveAt` is genuinely null; every reader in the platform
2924
+ * applies `canceledEffectiveAt ?? canceledAt` for that reason.
2925
+ */
2926
+ canceledAt: Date | null;
2927
+ canceledEffectiveAt: Date | null;
2555
2928
  }
2556
2929
  /** Snapshot of a `PlanVersion` row. */
2557
2930
  interface PlanVersionRecord {
@@ -2608,11 +2981,10 @@ interface SubscriptionRepository {
2608
2981
  countByBundleVersionId?(bundleVersionId: string): Promise<number>;
2609
2982
  /**
2610
2983
  * Counts active subscriptions (status `ACTIVE` or `TRIAL`) per plan key,
2611
- * platform-wide across all tenants of the project — feeds the tenant
2612
- * column of the SuperAdmin plan list (`GET /admin/catalog/plans/tenant-counts`).
2984
+ * platform-wide across every tenant — feeds the tenant column of the
2985
+ * SuperAdmin plan list (`GET /admin/catalog/plans/tenant-counts`).
2613
2986
  * Cross-version: counts the plan, not a single PlanVersion
2614
- * (subscriptions on superseded versions are included). `projectKey` is
2615
- * informational for single-project consumers.
2987
+ * (subscriptions on superseded versions are included).
2616
2988
  *
2617
2989
  * Returns a map `planKey → count`; plans without an active subscription
2618
2990
  * are missing (UI defaults to 0). Platform-wide count across all tenants →
@@ -2620,7 +2992,7 @@ interface SubscriptionRepository {
2620
2992
  *
2621
2993
  * Optional — if not implemented, the tenant column stays 0.
2622
2994
  */
2623
- countActiveByPlanKey?(projectKey: string): Promise<Record<string, number>>;
2995
+ countActiveByPlanKey?(): Promise<Record<string, number>>;
2624
2996
  }
2625
2997
  /**
2626
2998
  * Adapter for the `subscription_bundles` junction.
@@ -2712,6 +3084,40 @@ interface SubscriptionUsageRecord {
2712
3084
  /** Current period window — for proration and change-effective date. */
2713
3085
  currentPeriodStart: Date | null;
2714
3086
  currentPeriodEnd: Date | null;
3087
+ /**
3088
+ * End of what was committed to, which the period end need not equal.
3089
+ *
3090
+ * The cancellation rules measure against this: a subscription cancelled
3091
+ * inside its term keeps running until the term ends, not until the period
3092
+ * does. Null on a trial, and on any subscription written before the field
3093
+ * existed — readers treat that as "the period end is the answer".
3094
+ */
3095
+ minimumTermUntil?: Date | null;
3096
+ /**
3097
+ * The day of the month the subscription is billed on, 1–31.
3098
+ *
3099
+ * Read by the cancellation rules: a declaration after the notice window
3100
+ * lands one period past the term end, and that step has to measure from the
3101
+ * billing day rather than from a term end that may already have been
3102
+ * clamped by a short month.
3103
+ *
3104
+ * Optional, because an adapter that does not store the column keeps today's
3105
+ * behaviour — the step then takes its day from the term end, which is
3106
+ * correct except in the month after a clamp.
3107
+ */
3108
+ billingAnchorDay?: number | null;
3109
+ /**
3110
+ * When a cancellation was declared, and when it lands.
3111
+ *
3112
+ * Required for the reason the same pair is required on
3113
+ * `SubscriptionRecord`: the tenant billing route reads them to refuse a
3114
+ * plan change on a subscription that has ended, and a record that omits
3115
+ * them answers "not cancelled" — so the change is applied and prorated
3116
+ * while entitlement resolution, which reads a record that does carry them,
3117
+ * grants nothing.
3118
+ */
3119
+ canceledAt: Date | null;
3120
+ canceledEffectiveAt: Date | null;
2715
3121
  pendingPlan: string | null;
2716
3122
  pendingBillingCycle: string | null;
2717
3123
  pendingEffectiveAt: Date | null;
@@ -2787,12 +3193,29 @@ interface ImmediatePlanChangeInput {
2787
3193
  * change, or target package without trial). A `Date` is persisted.
2788
3194
  */
2789
3195
  trialEndsAt?: Date | null;
3196
+ /**
3197
+ * `canceledAt` as the caller read it, so the write can claim the row only
3198
+ * while that is still true.
3199
+ *
3200
+ * Three of the plan route's decisions depend on the cancellation — whether
3201
+ * the change is refused at all, whether the billing cycle may move, and
3202
+ * whether a fresh period is opened — and a read and a write are two
3203
+ * moments. A cancellation declared in between made every one of them answer
3204
+ * about a state that no longer existed, and the write went ahead anyway: a
3205
+ * plan term recorded past the date the subscription ends.
3206
+ *
3207
+ * `null` is a value here rather than an absence. It claims a row that has
3208
+ * no cancellation, and loses against one that has acquired one.
3209
+ */
3210
+ expectedCanceledAt: Date | null;
2790
3211
  }
2791
3212
  /** Input for `schedulePlanChange` (change at period end). */
2792
3213
  interface ScheduledPlanChangeInput {
2793
3214
  pendingPlan: string;
2794
3215
  pendingBillingCycle: string;
2795
3216
  pendingEffectiveAt: Date;
3217
+ /** See `ImmediatePlanChangeInput.expectedCanceledAt`. */
3218
+ expectedCanceledAt: Date | null;
2796
3219
  }
2797
3220
  /**
2798
3221
  * Input for `applyOnboardingSelection`. Plan-change fields that the
@@ -2801,6 +3224,12 @@ interface ScheduledPlanChangeInput {
2801
3224
  interface ApplyOnboardingSelectionInput {
2802
3225
  planId: string;
2803
3226
  cycle: string;
3227
+ /**
3228
+ * See `ImmediatePlanChangeInput.expectedCanceledAt`. The atomic path needs
3229
+ * it for the same reason the sequential one does: without it, the preferred
3230
+ * implementation is the one where the race stays open.
3231
+ */
3232
+ expectedCanceledAt: Date | null;
2804
3233
  /** For TRIAL → null, otherwise period start from `initialPeriodWindow`. */
2805
3234
  periodStart: Date | null;
2806
3235
  periodEnd: Date | null;
@@ -2817,6 +3246,12 @@ interface ApplyOnboardingSelectionResult {
2817
3246
  subscriptionId: string;
2818
3247
  /** null if no redeemPromo callback was provided or the callback returned null. */
2819
3248
  promoRedemption: PromoCodeRedemptionRecord | null;
3249
+ /**
3250
+ * False when the row's cancellation moved since the caller read it, in
3251
+ * which case nothing was written — including the promo redemption, which
3252
+ * shares the transaction.
3253
+ */
3254
+ claimed: boolean;
2820
3255
  }
2821
3256
  /**
2822
3257
  * Callback signature for promo-code redemption WITHIN the onboarding
@@ -2834,14 +3269,46 @@ type RedeemPromoInTransactionCallback = (tx: TransactionContext, subscriptionId:
2834
3269
  * app-specific. The platform service calls `invalidateTenant` in the
2835
3270
  * EntitlementService after a successful adapter call.
2836
3271
  */
3272
+ /** What `cancelSubscription` is told to write. Named so both adapters spell
3273
+ * the same shape once rather than each restating it. */
3274
+ interface CancelSubscriptionInput {
3275
+ canceledAt: Date;
3276
+ effectiveAt: Date;
3277
+ terminateNow: boolean;
3278
+ minimumTermUntil?: Date;
3279
+ }
3280
+ /** What `cancelSubscription` answers with. */
3281
+ interface CancelSubscriptionResult {
3282
+ canceledAt: Date | null;
3283
+ canceledEffectiveAt: Date | null;
3284
+ status: string;
3285
+ /**
3286
+ * True when a cancellation was already recorded and this call changed
3287
+ * nothing — the stored dates are returned instead.
3288
+ *
3289
+ * The caller checks first, but a check and a write are two moments, and two
3290
+ * requests can pass the check before either writes. Straddling a notice
3291
+ * deadline that costs a billing cycle: the first declaration lands on time,
3292
+ * the second recomputes against a later `now`, and an unconditional write
3293
+ * replaces the first answer with one a period further out. An
3294
+ * implementation therefore claims the row only while both cancellation
3295
+ * fields are still empty, and answers `true` here when the claim finds
3296
+ * nothing to claim.
3297
+ */
3298
+ alreadyCanceled: boolean;
3299
+ }
2837
3300
  interface TenantSubscriptionWritePort {
2838
3301
  /** Immediate change: set plan + cycle, clear pending fields, optionally reset the period. */
2839
3302
  changePlanImmediate(tenantId: string, input: ImmediatePlanChangeInput): Promise<{
2840
3303
  plan: string;
2841
3304
  billingCycle: string;
3305
+ /** False when the row's cancellation moved since the caller read it. */
3306
+ claimed: boolean;
2842
3307
  }>;
2843
3308
  /** Change at period end: set pending fields. */
2844
- schedulePlanChange(tenantId: string, input: ScheduledPlanChangeInput): Promise<void>;
3309
+ schedulePlanChange(tenantId: string, input: ScheduledPlanChangeInput): Promise<{
3310
+ claimed: boolean;
3311
+ }>;
2845
3312
  /**
2846
3313
  * Marks the pending PlanVersion as accepted. Idempotent — a duplicate
2847
3314
  * accept is a no-op. Returns `alreadyAccepted: true` if the status was
@@ -2854,13 +3321,30 @@ interface TenantSubscriptionWritePort {
2854
3321
  alreadyAccepted: boolean;
2855
3322
  }>;
2856
3323
  /**
2857
- * Cancel the subscription. `immediate=true` → status CANCELED from now;
2858
- * `false` → canceledAt = currentPeriodEnd, status is preserved.
3324
+ * Record a cancellation. The dates are decided above this port.
3325
+ *
3326
+ * `canceledAt` is when the customer said it; `effectiveAt` is when it
3327
+ * lands. They differ for every ordinary cancellation, because a
3328
+ * subscription cancelled inside its term keeps running, keeps being billed
3329
+ * and keeps its entitlements until the term ends. An adapter that computed
3330
+ * the second from the first — which this one did, as
3331
+ * `immediate ? now : currentPeriodEnd` — was deciding a commercial
3332
+ * question in a persistence layer, and could not see the minimum term or
3333
+ * the notice period at all.
3334
+ *
3335
+ * `terminateNow` flips the status immediately, and is set when the
3336
+ * cancellation is already effective: an operator ending a contract, or the
3337
+ * rules finding nothing left to run — no period, no term, as on a trial.
3338
+ * It is never a client's request. A tenant may always declare a
3339
+ * cancellation and may never shorten the term they are in; what decides
3340
+ * this flag is the date the rules returned, not the date they asked for.
3341
+ *
3342
+ * `minimumTermUntil` extends the stored commitment, and is set only when
3343
+ * the cancellation itself extends it: a declaration made after the notice
3344
+ * deadline buys the following period. Left unset the stored term end is
3345
+ * unchanged, which is the ordinary case.
2859
3346
  */
2860
- cancelSubscription(tenantId: string, immediate: boolean, now: Date): Promise<{
2861
- canceledAt: Date | null;
2862
- status: string;
2863
- }>;
3347
+ cancelSubscription(tenantId: string, input: CancelSubscriptionInput): Promise<CancelSubscriptionResult>;
2864
3348
  /**
2865
3349
  * Atomic onboarding creation: sets plan + cycle + period window
2866
3350
  * AND optionally calls a promo-redeem callback — all in a
@@ -3112,7 +3596,6 @@ interface RlsBypassPort {
3112
3596
 
3113
3597
  /** Filter for `PlanRepository.list()`. */
3114
3598
  interface PlanListFilter {
3115
- projectKey: string;
3116
3599
  /** Exclude soft-deleted plans — default `true`. */
3117
3600
  excludeDeleted?: boolean;
3118
3601
  /**
@@ -3146,7 +3629,7 @@ interface PlanListFilter {
3146
3629
  interface PlanRepository {
3147
3630
  list(filter: PlanListFilter): Promise<PlanRow[]>;
3148
3631
  findById(planId: string): Promise<PlanRow | null>;
3149
- findByKey(projectKey: string, planKey: string): Promise<PlanRow | null>;
3632
+ findByKey(planKey: string): Promise<PlanRow | null>;
3150
3633
  create(data: CreatePlanData): Promise<PlanRow>;
3151
3634
  update(planId: string, data: UpdatePlanData): Promise<PlanRow>;
3152
3635
  /** Sets `deletedAt` to NOW(); soft-deleted plans are filtered from `list` by default. */
@@ -3247,10 +3730,24 @@ interface PlanRepository {
3247
3730
  }
3248
3731
  /** Filter for `BundleRepository.list()`. */
3249
3732
  interface BundleListFilter {
3250
- projectKey: string;
3251
3733
  /** Exclude soft-deleted bundles — default `true`. */
3252
3734
  excludeDeleted?: boolean;
3253
3735
  }
3736
+ /**
3737
+ * What publishing a bundle draft records.
3738
+ *
3739
+ * Named because it was written out three times — the port, and each adapter's
3740
+ * implementation of it — and a signature restated is a contract restated: the
3741
+ * copies can drift, and nothing but a reader would notice.
3742
+ */
3743
+ interface PublishBundleVersionMeta {
3744
+ publishedByUserId: string | null;
3745
+ publishedChanges: VersionChange[];
3746
+ nonRegressive: boolean;
3747
+ /** Required — validated by the service before the repository call. */
3748
+ validFrom: Date;
3749
+ validUntil: Date | null;
3750
+ }
3254
3751
  /**
3255
3752
  * Adapter for `Bundle` + `BundleVersion` persistence. Consumers implement
3256
3753
  * this against their Prisma tables (`bundles` + `bundle_versions`).
@@ -3269,7 +3766,7 @@ interface BundleListFilter {
3269
3766
  interface BundleRepository {
3270
3767
  list(filter: BundleListFilter): Promise<BundleRow[]>;
3271
3768
  findById(bundleId: string): Promise<BundleRow | null>;
3272
- findByKey(projectKey: string, bundleKey: string): Promise<BundleRow | null>;
3769
+ findByKey(bundleKey: string): Promise<BundleRow | null>;
3273
3770
  create(data: CreateBundleData): Promise<BundleRow>;
3274
3771
  update(bundleId: string, data: UpdateBundleData): Promise<BundleRow>;
3275
3772
  /** Sets `deletedAt` to NOW(); soft-deleted bundles are filtered from `list` by default. */
@@ -3328,14 +3825,7 @@ interface BundleRepository {
3328
3825
  * if the predecessor carries a `validUntil` — the adapter only
3329
3826
  * persists, it does not validate again.
3330
3827
  */
3331
- publishDraft(versionId: string, publishMeta: {
3332
- publishedByUserId: string | null;
3333
- publishedChanges: VersionChange[];
3334
- nonRegressive: boolean;
3335
- /** Required — validated by the service before the repository call. */
3336
- validFrom: Date;
3337
- validUntil: Date | null;
3338
- }, tx?: TransactionContext): Promise<BundleVersionRow>;
3828
+ publishDraft(versionId: string, publishMeta: PublishBundleVersionMeta, tx?: TransactionContext): Promise<BundleVersionRow>;
3339
3829
  /**
3340
3830
  * Hard-discards a draft version (`publishedAt === null`) from the DB.
3341
3831
  * Throws if the version was already published — published versions
@@ -3373,7 +3863,6 @@ interface MarketingProjectionRepository {
3373
3863
  }
3374
3864
  /** Upsert input for a capability from the discovery sync. */
3375
3865
  interface UpsertCapabilityEntryData {
3376
- projectKey: string;
3377
3866
  capabilityKey: string;
3378
3867
  label: string;
3379
3868
  description: string | null;
@@ -3390,7 +3879,6 @@ interface UpsertCapabilityEntryData {
3390
3879
  }
3391
3880
  /** Upsert input for a feature from the discovery sync. */
3392
3881
  interface UpsertFeatureEntryData {
3393
- projectKey: string;
3394
3882
  featureKey: string;
3395
3883
  label: string;
3396
3884
  description: string | null;
@@ -3404,7 +3892,6 @@ interface UpsertFeatureEntryData {
3404
3892
  }
3405
3893
  /** Upsert input for a quota from the discovery sync. */
3406
3894
  interface UpsertQuotaEntryData {
3407
- projectKey: string;
3408
3895
  quotaKey: string;
3409
3896
  label: string;
3410
3897
  description: string | null;
@@ -3434,7 +3921,7 @@ interface SetCatalogEntryReviewData {
3434
3921
  * Prisma tables.
3435
3922
  *
3436
3923
  * Binding:
3437
- * - `upsert*` matches on (`projectKey`, `<key>`) and leaves `i18n`,
3924
+ * - `upsert*` matches on `<key>` and leaves `i18n`,
3438
3925
  * `sortOrder`, `createdAt` as well as the approval fields (`approvedAt`/
3439
3926
  * `approvedBy`/`approvedSignature`) **untouched** on an update —
3440
3927
  * only the code-derived fields + the status (resolved by the service)
@@ -3450,7 +3937,7 @@ interface CatalogEntryRepository {
3450
3937
  upsertCapability(data: UpsertCapabilityEntryData): Promise<CapabilityCatalogEntryRow>;
3451
3938
  upsertFeature(data: UpsertFeatureEntryData): Promise<FeatureCatalogEntryRow>;
3452
3939
  upsertQuota(data: UpsertQuotaEntryData): Promise<QuotaCatalogEntryRow>;
3453
- retireMissing(projectKey: string, type: 'capability' | 'feature' | 'quota', presentKeys: string[]): Promise<number>;
3940
+ retireMissing(type: 'capability' | 'feature' | 'quota', presentKeys: string[]): Promise<number>;
3454
3941
  /**
3455
3942
  * Sets or clears the successor pointer of a feature/quota
3456
3943
  * (#39). The sync calls this when a key disappears from the snapshot
@@ -3459,17 +3946,17 @@ interface CatalogEntryRepository {
3459
3946
  * adapters without a `successor_key` column omit the methods, and the sync
3460
3947
  * then skips the pointers with a warn log.
3461
3948
  */
3462
- setFeatureSuccessor?(projectKey: string, featureKey: string, successorKey: string | null): Promise<FeatureCatalogEntryRow>;
3463
- setQuotaSuccessor?(projectKey: string, quotaKey: string, successorKey: string | null): Promise<QuotaCatalogEntryRow>;
3464
- findFeature(projectKey: string, featureKey: string): Promise<FeatureCatalogEntryRow | null>;
3465
- findQuota(projectKey: string, quotaKey: string): Promise<QuotaCatalogEntryRow | null>;
3466
- setFeatureReview(projectKey: string, featureKey: string, data: SetCatalogEntryReviewData): Promise<FeatureCatalogEntryRow>;
3467
- setQuotaReview(projectKey: string, quotaKey: string, data: SetCatalogEntryReviewData): Promise<QuotaCatalogEntryRow>;
3468
- setFeatureI18n(projectKey: string, featureKey: string, i18n: CatalogEntryI18n): Promise<FeatureCatalogEntryRow>;
3469
- setQuotaI18n(projectKey: string, quotaKey: string, i18n: CatalogEntryI18n): Promise<QuotaCatalogEntryRow>;
3949
+ setFeatureSuccessor?(featureKey: string, successorKey: string | null): Promise<FeatureCatalogEntryRow>;
3950
+ setQuotaSuccessor?(quotaKey: string, successorKey: string | null): Promise<QuotaCatalogEntryRow>;
3951
+ findFeature(featureKey: string): Promise<FeatureCatalogEntryRow | null>;
3952
+ findQuota(quotaKey: string): Promise<QuotaCatalogEntryRow | null>;
3953
+ setFeatureReview(featureKey: string, data: SetCatalogEntryReviewData): Promise<FeatureCatalogEntryRow>;
3954
+ setQuotaReview(quotaKey: string, data: SetCatalogEntryReviewData): Promise<QuotaCatalogEntryRow>;
3955
+ setFeatureI18n(featureKey: string, i18n: CatalogEntryI18n): Promise<FeatureCatalogEntryRow>;
3956
+ setQuotaI18n(quotaKey: string, i18n: CatalogEntryI18n): Promise<QuotaCatalogEntryRow>;
3470
3957
  /** Sets the editable base fields (default locale `label`/`description`). */
3471
- setFeatureBase(projectKey: string, featureKey: string, data: UpdateCatalogEntryBaseData): Promise<FeatureCatalogEntryRow>;
3472
- setQuotaBase(projectKey: string, quotaKey: string, data: UpdateCatalogEntryBaseData): Promise<QuotaCatalogEntryRow>;
3958
+ setFeatureBase(featureKey: string, data: UpdateCatalogEntryBaseData): Promise<FeatureCatalogEntryRow>;
3959
+ setQuotaBase(quotaKey: string, data: UpdateCatalogEntryBaseData): Promise<QuotaCatalogEntryRow>;
3473
3960
  }
3474
3961
  /**
3475
3962
  * Adapter for `promotions`. **No versioning** — promotions are edited
@@ -3477,7 +3964,7 @@ interface CatalogEntryRepository {
3477
3964
  * this against their `promotions` Prisma table.
3478
3965
  */
3479
3966
  interface PromotionRepository {
3480
- list(filter: PromotionFilter): Promise<PromotionRow[]>;
3967
+ list(): Promise<PromotionRow[]>;
3481
3968
  findById(id: string): Promise<PromotionRow | null>;
3482
3969
  create(data: CreatePromotionData): Promise<PromotionRow>;
3483
3970
  update(id: string, data: UpdatePromotionData): Promise<PromotionRow>;
@@ -3485,13 +3972,14 @@ interface PromotionRepository {
3485
3972
  delete(id: string): Promise<void>;
3486
3973
  }
3487
3974
  /**
3488
- * Adapter for `marketing_settings` — one row per project. `get` returns
3489
- * `null` as long as the SuperAdmin has saved nothing (then the full
3490
- * `availableLocales` pool counts as active). `upsert` creates the row or replaces it.
3975
+ * Adapter for `marketing_settings` — at most one row, which a `CHECK` on the
3976
+ * canonical schema holds rather than convention. `get` returns `null` as long
3977
+ * as the SuperAdmin has saved nothing (then the full `availableLocales` pool
3978
+ * counts as active). `upsert` creates the row or replaces it.
3491
3979
  */
3492
3980
  interface MarketingSettingsRepository {
3493
- get(projectKey: string): Promise<MarketingSettingsRow | null>;
3494
- upsert(projectKey: string, data: UpdateMarketingSettingsData): Promise<MarketingSettingsRow>;
3981
+ get(): Promise<MarketingSettingsRow | null>;
3982
+ upsert(data: UpdateMarketingSettingsData): Promise<MarketingSettingsRow>;
3495
3983
  }
3496
3984
 
3497
3985
  /**
@@ -3508,6 +3996,62 @@ interface CheckoutOfferRepository {
3508
3996
  consume(id: string): Promise<CheckoutOfferRow>;
3509
3997
  }
3510
3998
 
3999
+ /** Which changes to list. */
4000
+ interface SettingsChangeFilter {
4001
+ /** Only changes an operator has, or has not, acknowledged. Omitted: both. */
4002
+ acknowledged?: boolean;
4003
+ /** The most recently recorded ones. Omitted: every matching change. */
4004
+ limit?: number;
4005
+ }
4006
+ /**
4007
+ * Stores what the installation applied and what changed between two boots.
4008
+ *
4009
+ * `applied_settings` holds one row for the installation; `settings_changes`
4010
+ * holds one row per boot that found the fingerprint moved. An adapter
4011
+ * translates: it does not decide what a change is, and it does not read the
4012
+ * row back into anything that runs.
4013
+ *
4014
+ * Both writes are guarded on the fingerprint the caller read. Several replicas
4015
+ * of one installation start together after one edit of the file, each reads
4016
+ * the same record and each finds the same difference; the guard is what makes
4017
+ * one of them the boot that recorded it and the others boots that found it
4018
+ * recorded. Without it every replica would write the change and mail the
4019
+ * addresses, once per replica.
4020
+ */
4021
+ interface AppliedSettingsPort {
4022
+ /** The record, or null before the first boot that could write one. */
4023
+ readApplied(): Promise<AppliedSettingsRecord | null>;
4024
+ /**
4025
+ * Replaces the installation's record — there is only ever the one row —
4026
+ * provided the stored record still carries `expectedFingerprint`: the
4027
+ * fingerprint the caller read, or `null` where it read no record. Returns
4028
+ * whether it did. `false` means the record moved between the caller's read
4029
+ * and this write, and nothing was written: another boot got there first.
4030
+ */
4031
+ writeApplied(record: AppliedSettingsRecord, expectedFingerprint: string | null): Promise<boolean>;
4032
+ /**
4033
+ * Appends a change a boot noticed and replaces the record it supersedes, in
4034
+ * one step: both land, or neither does. Guarded like `writeApplied`, on the
4035
+ * fingerprint of the record the change was noticed against. Returns the
4036
+ * change as stored — the id is the adapter's to assign — or `null` where
4037
+ * the record had already moved on: another boot noticed first, and the
4038
+ * change is that boot's to report.
4039
+ */
4040
+ recordChange(change: NewSettingsChange, record: AppliedSettingsRecord, expectedFingerprint: string): Promise<SettingsChangeRecord | null>;
4041
+ /**
4042
+ * Changes, the most recently recorded first: the order the record went
4043
+ * through them, which the database numbers at each write — not the order
4044
+ * of the moments they carry, which are the recording starts' clocks.
4045
+ */
4046
+ listChanges(filter?: SettingsChangeFilter): Promise<SettingsChangeRecord[]>;
4047
+ /**
4048
+ * Marks a change as seen. Returns the updated record, or null where no
4049
+ * change has that id. A change already acknowledged keeps its first
4050
+ * acknowledgement — repeating the action changes nothing.
4051
+ */
4052
+ acknowledgeChange(id: string, acknowledgedBy: string, acknowledgedAt: Date): Promise<SettingsChangeRecord | null>;
4053
+ }
4054
+
3511
4055
  /** Class reference usable as a DI token (e.g. the consumer's `PrismaService`). */
3512
4056
  type PersistenceClassRef = abstract new (...args: never[]) => unknown;
3513
4057
  /** DI token forms a persistence bundle may reference in `inject`. */
@@ -3565,6 +4109,12 @@ interface SaaSiCatPersistenceCore {
3565
4109
  auditQuery?: PersistenceProvider<AuditQueryPort>;
3566
4110
  /** Aggregation for the admin stats dashboard. */
3567
4111
  auditStats?: PersistenceProvider<AuditStatsPort>;
4112
+ /**
4113
+ * The record of the applied configuration (`SettingsModule`). Optional so
4114
+ * an adapter written before it existed keeps working; without it the
4115
+ * platform says once at boot that it is not recording.
4116
+ */
4117
+ appliedSettings?: PersistenceProvider<AppliedSettingsPort>;
3568
4118
  }
3569
4119
  /** Repositories for the entitlement/contract loop (`EntitlementModule`). */
3570
4120
  interface SaaSiCatPersistenceEntitlement {
@@ -3703,6 +4253,8 @@ declare const CATALOG_ERROR_CODES: {
3703
4253
  readonly BUNDLE_VERSION_SUPERSEDED: "BUNDLE_VERSION_SUPERSEDED";
3704
4254
  readonly BUNDLE_VERSION_REGRESSION: "BUNDLE_VERSION_REGRESSION";
3705
4255
  readonly BUNDLE_VERSION_ZERO_PRICE: "BUNDLE_VERSION_ZERO_PRICE";
4256
+ readonly BUNDLE_VERSION_NO_PRICE: "BUNDLE_VERSION_NO_PRICE";
4257
+ readonly BUNDLE_VERSION_NOT_PRICED_FOR_PLAN: "BUNDLE_VERSION_NOT_PRICED_FOR_PLAN";
3706
4258
  readonly BUNDLE_VERSION_DISCARD_NOT_IMPLEMENTED: "BUNDLE_VERSION_DISCARD_NOT_IMPLEMENTED";
3707
4259
  readonly BUNDLE_VERSION_VALID_FROM_REQUIRED: "BUNDLE_VERSION_VALID_FROM_REQUIRED";
3708
4260
  readonly BUNDLE_VERSION_VALID_FROM_INVALID: "BUNDLE_VERSION_VALID_FROM_INVALID";
@@ -3730,6 +4282,10 @@ declare const CATALOG_ERROR_CODES: {
3730
4282
  readonly QUOTA_NOT_IN_DISCOVERY_SNAPSHOT: "QUOTA_NOT_IN_DISCOVERY_SNAPSHOT";
3731
4283
  readonly DISCOVERY_STATUS_TRANSITION_INVALID: "DISCOVERY_STATUS_TRANSITION_INVALID";
3732
4284
  readonly DISCOVERY_NOT_INITIALIZED: "DISCOVERY_NOT_INITIALIZED";
4285
+ /** The uploaded document is not a plan catalog — unparseable, or not an object. */
4286
+ readonly PLAN_CATALOG_UNREADABLE: "PLAN_CATALOG_UNREADABLE";
4287
+ /** It parsed, and then failed the schema or a cross-field rule. */
4288
+ readonly PLAN_CATALOG_INVALID: "PLAN_CATALOG_INVALID";
3733
4289
  };
3734
4290
  type CatalogErrorCode = (typeof CATALOG_ERROR_CODES)[keyof typeof CATALOG_ERROR_CODES];
3735
4291
  /** Bundle bookings on a tenant subscription. */
@@ -3743,6 +4299,8 @@ declare const BILLING_ERROR_CODES: {
3743
4299
  readonly BUNDLE_ALREADY_SUBSCRIBED: "BUNDLE_ALREADY_SUBSCRIBED";
3744
4300
  readonly BUNDLE_INCOMPATIBLE_WITH_PLAN: "BUNDLE_INCOMPATIBLE_WITH_PLAN";
3745
4301
  readonly BUNDLE_NOT_SELF_SERVICE: "BUNDLE_NOT_SELF_SERVICE";
4302
+ readonly BUNDLE_CYCLE_EXCEEDS_PLAN: "BUNDLE_CYCLE_EXCEEDS_PLAN";
4303
+ readonly BUNDLE_NOT_PRICED_FOR_THIS_PLAN: "BUNDLE_NOT_PRICED_FOR_THIS_PLAN";
3746
4304
  readonly SUBSCRIPTION_BUNDLE_ALREADY_CANCELLED: "SUBSCRIPTION_BUNDLE_ALREADY_CANCELLED";
3747
4305
  readonly SUBSCRIPTION_BUNDLE_NOT_CANCELLED: "SUBSCRIPTION_BUNDLE_NOT_CANCELLED";
3748
4306
  readonly SUBSCRIPTION_BUNDLE_CANCELLATION_EFFECTIVE: "SUBSCRIPTION_BUNDLE_CANCELLATION_EFFECTIVE";
@@ -3758,6 +4316,72 @@ declare const BILLING_ERROR_CODES: {
3758
4316
  readonly PLAN_NOT_SELF_SERVICE: "PLAN_NOT_SELF_SERVICE";
3759
4317
  /** Plan change refused. Carries `blockers[]` with their own codes. */
3760
4318
  readonly PLAN_CHANGE_BLOCKED: "PLAN_CHANGE_BLOCKED";
4319
+ /**
4320
+ * The subscription moved between the read a request was decided on and the
4321
+ * write it attempted, so nothing was written. The caller reloads and asks
4322
+ * again.
4323
+ */
4324
+ readonly SUBSCRIPTION_CHANGED: "SUBSCRIPTION_CHANGED";
4325
+ /**
4326
+ * The tenant has no subscription to act on.
4327
+ *
4328
+ * `SUBSCRIPTION_NOT_FOUND` states the same fact on the read routes. Both
4329
+ * are already on the wire and a code is renamed only deliberately, so both
4330
+ * are named here rather than one being dropped behind a consumer's back.
4331
+ */
4332
+ readonly NO_SUBSCRIPTION: "NO_SUBSCRIPTION";
4333
+ /**
4334
+ * The cancellation date the reader was shown is no longer the one the rules
4335
+ * return, so the confirmation is refused rather than silently applied.
4336
+ * Carries the recomputed dates, so the page can re-ask instead of guessing.
4337
+ */
4338
+ readonly CANCELLATION_TERMS_CHANGED: "CANCELLATION_TERMS_CHANGED";
4339
+ /** The subscription has ended; its plan can no longer be changed. */
4340
+ readonly SUBSCRIPTION_ENDED: "SUBSCRIPTION_ENDED";
4341
+ /** An active special contract blocks self-service plan changes. */
4342
+ readonly PLAN_LOCKED: "PLAN_LOCKED";
4343
+ /** Current usage of one quota exceeds what the target plan allows. */
4344
+ readonly QUOTA_OVER_TARGET: "QUOTA_OVER_TARGET";
4345
+ /** The change drops features the tenant has today. */
4346
+ readonly FEATURE_LOST: "FEATURE_LOST";
4347
+ readonly FEATURES_LOST: "FEATURES_LOST";
4348
+ /** Target plan and cycle already match what is in place. */
4349
+ readonly NO_CHANGE: "NO_CHANGE";
4350
+ /** A shorter cycle cannot start inside the term already running. */
4351
+ readonly CYCLE_SHORTENS_AT_TERM_END: "CYCLE_SHORTENS_AT_TERM_END";
4352
+ /** A cancelled subscription cannot change its billing cycle. */
4353
+ readonly CANCELLATION_LOCKS_THE_CYCLE: "CANCELLATION_LOCKS_THE_CYCLE";
4354
+ /**
4355
+ * A bundle the tenant already holds runs past the cycle they are moving to.
4356
+ *
4357
+ * Its own code rather than `BUNDLE_CYCLE_EXCEEDS_PLAN`, which states the
4358
+ * same rule about a booking that has not been made yet. The two need
4359
+ * different sentences: this one can name the day the obstacle lifts and
4360
+ * tell the reader to cancel the booking, and that advice is wrong for
4361
+ * someone who is only about to book. One template cannot serve both.
4362
+ */
4363
+ readonly BUNDLE_BOOKING_OUTLASTS_TARGET_CYCLE: "BUNDLE_BOOKING_OUTLASTS_TARGET_CYCLE";
4364
+ /**
4365
+ * Features of the previewed bundle are already covered by the plan or by
4366
+ * another booked bundle. A warning rather than a blocker: paying twice is
4367
+ * the customer's decision, and the preview only has to say so first.
4368
+ */
4369
+ readonly REDUNDANT_FEATURES: "REDUNDANT_FEATURES";
4370
+ /**
4371
+ * A booking's minimum term outlasts the period being cancelled, so the
4372
+ * cancellation takes effect at the end of the term, not of the period.
4373
+ */
4374
+ readonly MINIMUM_TERM_BINDS: "MINIMUM_TERM_BINDS";
4375
+ /**
4376
+ * The previewed bundle requires features that neither the plan nor an
4377
+ * active booking provides.
4378
+ *
4379
+ * The same string is a `StrictModeWarningCode` in `bundle.types.ts`, where
4380
+ * it names the catalogue-authoring reading of the rule and travels with its
4381
+ * own message. This declaration is the booking preview's blocker, which a
4382
+ * tenant reads and therefore needs a shipped text for.
4383
+ */
4384
+ readonly BUNDLE_FEATURE_DEPENDENCY_UNSATISFIED: "BUNDLE_FEATURE_DEPENDENCY_UNSATISFIED";
3761
4385
  readonly NO_PENDING_PLAN_VERSION: "NO_PENDING_PLAN_VERSION";
3762
4386
  readonly ONBOARDING_CREATE_FAILED: "ONBOARDING_CREATE_FAILED";
3763
4387
  readonly BUNDLE_PREVIEW_ARGUMENT_AMBIGUOUS: "BUNDLE_PREVIEW_ARGUMENT_AMBIGUOUS";
@@ -3782,6 +4406,8 @@ declare const CONTRACT_ERROR_CODES: {
3782
4406
  readonly SUBSCRIPTION_CONTRACT_PLAN_LINE_ITEM_REQUIRED: "SUBSCRIPTION_CONTRACT_PLAN_LINE_ITEM_REQUIRED";
3783
4407
  readonly SUBSCRIPTION_CONTRACT_INVALID_DATE: "SUBSCRIPTION_CONTRACT_INVALID_DATE";
3784
4408
  readonly SUBSCRIPTION_CONTRACT_INVALID_WINDOW: "SUBSCRIPTION_CONTRACT_INVALID_WINDOW";
4409
+ readonly SUBSCRIPTION_CONTRACT_LINE_ITEM_TAX_MISMATCH: "SUBSCRIPTION_CONTRACT_LINE_ITEM_TAX_MISMATCH";
4410
+ readonly SUBSCRIPTION_CONTRACT_LINE_ITEM_CURRENCY_MISMATCH: "SUBSCRIPTION_CONTRACT_LINE_ITEM_CURRENCY_MISMATCH";
3785
4411
  readonly SUBSCRIPTION_CONTRACT_TERMINATION_BEFORE_START: "SUBSCRIPTION_CONTRACT_TERMINATION_BEFORE_START";
3786
4412
  readonly CHECKOUT_OFFER_NOT_FOUND: "CHECKOUT_OFFER_NOT_FOUND";
3787
4413
  readonly CHECKOUT_OFFER_EXPIRED: "CHECKOUT_OFFER_EXPIRED";
@@ -3850,6 +4476,12 @@ declare const PROMO_ERROR_CODES: {
3850
4476
  readonly PROMO_MAX_REDEMPTIONS_LOWERED: "PROMO_MAX_REDEMPTIONS_LOWERED";
3851
4477
  };
3852
4478
  type PromoErrorCode = (typeof PROMO_ERROR_CODES)[keyof typeof PROMO_ERROR_CODES];
4479
+ /** Codes of the settings record (`GET /admin/settings`, the acknowledgement). */
4480
+ declare const SETTINGS_ERROR_CODES: {
4481
+ /** No recorded change has this id, or the installation keeps no record at all. */
4482
+ readonly SETTINGS_CHANGE_NOT_FOUND: "SETTINGS_CHANGE_NOT_FOUND";
4483
+ };
4484
+ type SettingsErrorCode = (typeof SETTINGS_ERROR_CODES)[keyof typeof SETTINGS_ERROR_CODES];
3853
4485
  /**
3854
4486
  * Every exception code the platform emits, in one object.
3855
4487
  *
@@ -3858,6 +4490,8 @@ type PromoErrorCode = (typeof PROMO_ERROR_CODES)[keyof typeof PROMO_ERROR_CODES]
3858
4490
  * removing one may not.
3859
4491
  */
3860
4492
  declare const PLATFORM_ERROR_CODES: {
4493
+ /** No recorded change has this id, or the installation keeps no record at all. */
4494
+ readonly SETTINGS_CHANGE_NOT_FOUND: "SETTINGS_CHANGE_NOT_FOUND";
3861
4495
  readonly PENDING_REGISTRATION_NOT_FOUND: "PENDING_REGISTRATION_NOT_FOUND";
3862
4496
  readonly PENDING_REGISTRATION_EXPIRED: "PENDING_REGISTRATION_EXPIRED";
3863
4497
  readonly INVALID_REGISTRATION_STATE: "INVALID_REGISTRATION_STATE";
@@ -3882,6 +4516,8 @@ declare const PLATFORM_ERROR_CODES: {
3882
4516
  readonly SUBSCRIPTION_CONTRACT_PLAN_LINE_ITEM_REQUIRED: "SUBSCRIPTION_CONTRACT_PLAN_LINE_ITEM_REQUIRED";
3883
4517
  readonly SUBSCRIPTION_CONTRACT_INVALID_DATE: "SUBSCRIPTION_CONTRACT_INVALID_DATE";
3884
4518
  readonly SUBSCRIPTION_CONTRACT_INVALID_WINDOW: "SUBSCRIPTION_CONTRACT_INVALID_WINDOW";
4519
+ readonly SUBSCRIPTION_CONTRACT_LINE_ITEM_TAX_MISMATCH: "SUBSCRIPTION_CONTRACT_LINE_ITEM_TAX_MISMATCH";
4520
+ readonly SUBSCRIPTION_CONTRACT_LINE_ITEM_CURRENCY_MISMATCH: "SUBSCRIPTION_CONTRACT_LINE_ITEM_CURRENCY_MISMATCH";
3885
4521
  readonly SUBSCRIPTION_CONTRACT_TERMINATION_BEFORE_START: "SUBSCRIPTION_CONTRACT_TERMINATION_BEFORE_START";
3886
4522
  readonly CHECKOUT_OFFER_NOT_FOUND: "CHECKOUT_OFFER_NOT_FOUND";
3887
4523
  readonly CHECKOUT_OFFER_EXPIRED: "CHECKOUT_OFFER_EXPIRED";
@@ -3899,6 +4535,8 @@ declare const PLATFORM_ERROR_CODES: {
3899
4535
  readonly BUNDLE_ALREADY_SUBSCRIBED: "BUNDLE_ALREADY_SUBSCRIBED";
3900
4536
  readonly BUNDLE_INCOMPATIBLE_WITH_PLAN: "BUNDLE_INCOMPATIBLE_WITH_PLAN";
3901
4537
  readonly BUNDLE_NOT_SELF_SERVICE: "BUNDLE_NOT_SELF_SERVICE";
4538
+ readonly BUNDLE_CYCLE_EXCEEDS_PLAN: "BUNDLE_CYCLE_EXCEEDS_PLAN";
4539
+ readonly BUNDLE_NOT_PRICED_FOR_THIS_PLAN: "BUNDLE_NOT_PRICED_FOR_THIS_PLAN";
3902
4540
  readonly SUBSCRIPTION_BUNDLE_ALREADY_CANCELLED: "SUBSCRIPTION_BUNDLE_ALREADY_CANCELLED";
3903
4541
  readonly SUBSCRIPTION_BUNDLE_NOT_CANCELLED: "SUBSCRIPTION_BUNDLE_NOT_CANCELLED";
3904
4542
  readonly SUBSCRIPTION_BUNDLE_CANCELLATION_EFFECTIVE: "SUBSCRIPTION_BUNDLE_CANCELLATION_EFFECTIVE";
@@ -3914,6 +4552,72 @@ declare const PLATFORM_ERROR_CODES: {
3914
4552
  readonly PLAN_NOT_SELF_SERVICE: "PLAN_NOT_SELF_SERVICE";
3915
4553
  /** Plan change refused. Carries `blockers[]` with their own codes. */
3916
4554
  readonly PLAN_CHANGE_BLOCKED: "PLAN_CHANGE_BLOCKED";
4555
+ /**
4556
+ * The subscription moved between the read a request was decided on and the
4557
+ * write it attempted, so nothing was written. The caller reloads and asks
4558
+ * again.
4559
+ */
4560
+ readonly SUBSCRIPTION_CHANGED: "SUBSCRIPTION_CHANGED";
4561
+ /**
4562
+ * The tenant has no subscription to act on.
4563
+ *
4564
+ * `SUBSCRIPTION_NOT_FOUND` states the same fact on the read routes. Both
4565
+ * are already on the wire and a code is renamed only deliberately, so both
4566
+ * are named here rather than one being dropped behind a consumer's back.
4567
+ */
4568
+ readonly NO_SUBSCRIPTION: "NO_SUBSCRIPTION";
4569
+ /**
4570
+ * The cancellation date the reader was shown is no longer the one the rules
4571
+ * return, so the confirmation is refused rather than silently applied.
4572
+ * Carries the recomputed dates, so the page can re-ask instead of guessing.
4573
+ */
4574
+ readonly CANCELLATION_TERMS_CHANGED: "CANCELLATION_TERMS_CHANGED";
4575
+ /** The subscription has ended; its plan can no longer be changed. */
4576
+ readonly SUBSCRIPTION_ENDED: "SUBSCRIPTION_ENDED";
4577
+ /** An active special contract blocks self-service plan changes. */
4578
+ readonly PLAN_LOCKED: "PLAN_LOCKED";
4579
+ /** Current usage of one quota exceeds what the target plan allows. */
4580
+ readonly QUOTA_OVER_TARGET: "QUOTA_OVER_TARGET";
4581
+ /** The change drops features the tenant has today. */
4582
+ readonly FEATURE_LOST: "FEATURE_LOST";
4583
+ readonly FEATURES_LOST: "FEATURES_LOST";
4584
+ /** Target plan and cycle already match what is in place. */
4585
+ readonly NO_CHANGE: "NO_CHANGE";
4586
+ /** A shorter cycle cannot start inside the term already running. */
4587
+ readonly CYCLE_SHORTENS_AT_TERM_END: "CYCLE_SHORTENS_AT_TERM_END";
4588
+ /** A cancelled subscription cannot change its billing cycle. */
4589
+ readonly CANCELLATION_LOCKS_THE_CYCLE: "CANCELLATION_LOCKS_THE_CYCLE";
4590
+ /**
4591
+ * A bundle the tenant already holds runs past the cycle they are moving to.
4592
+ *
4593
+ * Its own code rather than `BUNDLE_CYCLE_EXCEEDS_PLAN`, which states the
4594
+ * same rule about a booking that has not been made yet. The two need
4595
+ * different sentences: this one can name the day the obstacle lifts and
4596
+ * tell the reader to cancel the booking, and that advice is wrong for
4597
+ * someone who is only about to book. One template cannot serve both.
4598
+ */
4599
+ readonly BUNDLE_BOOKING_OUTLASTS_TARGET_CYCLE: "BUNDLE_BOOKING_OUTLASTS_TARGET_CYCLE";
4600
+ /**
4601
+ * Features of the previewed bundle are already covered by the plan or by
4602
+ * another booked bundle. A warning rather than a blocker: paying twice is
4603
+ * the customer's decision, and the preview only has to say so first.
4604
+ */
4605
+ readonly REDUNDANT_FEATURES: "REDUNDANT_FEATURES";
4606
+ /**
4607
+ * A booking's minimum term outlasts the period being cancelled, so the
4608
+ * cancellation takes effect at the end of the term, not of the period.
4609
+ */
4610
+ readonly MINIMUM_TERM_BINDS: "MINIMUM_TERM_BINDS";
4611
+ /**
4612
+ * The previewed bundle requires features that neither the plan nor an
4613
+ * active booking provides.
4614
+ *
4615
+ * The same string is a `StrictModeWarningCode` in `bundle.types.ts`, where
4616
+ * it names the catalogue-authoring reading of the rule and travels with its
4617
+ * own message. This declaration is the booking preview's blocker, which a
4618
+ * tenant reads and therefore needs a shipped text for.
4619
+ */
4620
+ readonly BUNDLE_FEATURE_DEPENDENCY_UNSATISFIED: "BUNDLE_FEATURE_DEPENDENCY_UNSATISFIED";
3917
4621
  readonly NO_PENDING_PLAN_VERSION: "NO_PENDING_PLAN_VERSION";
3918
4622
  readonly ONBOARDING_CREATE_FAILED: "ONBOARDING_CREATE_FAILED";
3919
4623
  readonly BUNDLE_PREVIEW_ARGUMENT_AMBIGUOUS: "BUNDLE_PREVIEW_ARGUMENT_AMBIGUOUS";
@@ -3952,6 +4656,8 @@ declare const PLATFORM_ERROR_CODES: {
3952
4656
  readonly BUNDLE_VERSION_SUPERSEDED: "BUNDLE_VERSION_SUPERSEDED";
3953
4657
  readonly BUNDLE_VERSION_REGRESSION: "BUNDLE_VERSION_REGRESSION";
3954
4658
  readonly BUNDLE_VERSION_ZERO_PRICE: "BUNDLE_VERSION_ZERO_PRICE";
4659
+ readonly BUNDLE_VERSION_NO_PRICE: "BUNDLE_VERSION_NO_PRICE";
4660
+ readonly BUNDLE_VERSION_NOT_PRICED_FOR_PLAN: "BUNDLE_VERSION_NOT_PRICED_FOR_PLAN";
3955
4661
  readonly BUNDLE_VERSION_DISCARD_NOT_IMPLEMENTED: "BUNDLE_VERSION_DISCARD_NOT_IMPLEMENTED";
3956
4662
  readonly BUNDLE_VERSION_VALID_FROM_REQUIRED: "BUNDLE_VERSION_VALID_FROM_REQUIRED";
3957
4663
  readonly BUNDLE_VERSION_VALID_FROM_INVALID: "BUNDLE_VERSION_VALID_FROM_INVALID";
@@ -3979,6 +4685,10 @@ declare const PLATFORM_ERROR_CODES: {
3979
4685
  readonly QUOTA_NOT_IN_DISCOVERY_SNAPSHOT: "QUOTA_NOT_IN_DISCOVERY_SNAPSHOT";
3980
4686
  readonly DISCOVERY_STATUS_TRANSITION_INVALID: "DISCOVERY_STATUS_TRANSITION_INVALID";
3981
4687
  readonly DISCOVERY_NOT_INITIALIZED: "DISCOVERY_NOT_INITIALIZED";
4688
+ /** The uploaded document is not a plan catalog — unparseable, or not an object. */
4689
+ readonly PLAN_CATALOG_UNREADABLE: "PLAN_CATALOG_UNREADABLE";
4690
+ /** It parsed, and then failed the schema or a cross-field rule. */
4691
+ readonly PLAN_CATALOG_INVALID: "PLAN_CATALOG_INVALID";
3982
4692
  readonly PROMO_CODE_NOT_FOUND: "PROMO_CODE_NOT_FOUND";
3983
4693
  readonly PROMO_CODE_ALREADY_EXISTS: "PROMO_CODE_ALREADY_EXISTS";
3984
4694
  readonly PROMO_CODE_HAS_REDEMPTIONS: "PROMO_CODE_HAS_REDEMPTIONS";
@@ -4021,7 +4731,7 @@ declare const PLATFORM_ERROR_CODES: {
4021
4731
  /** Email already taken (mapped from `PlatformUserExistsError`). */
4022
4732
  readonly EMAIL_EXISTS: "EMAIL_EXISTS";
4023
4733
  };
4024
- type PlatformErrorCode = SetupErrorCode | AuthErrorCode | PromoErrorCode | CatalogErrorCode | BillingErrorCode | ContractErrorCode | RegistrationErrorCode;
4734
+ type PlatformErrorCode = SetupErrorCode | AuthErrorCode | PromoErrorCode | CatalogErrorCode | BillingErrorCode | ContractErrorCode | RegistrationErrorCode | SettingsErrorCode;
4025
4735
  /**
4026
4736
  * Shape of a coded error response.
4027
4737
  *
@@ -4766,6 +5476,101 @@ interface SetupConfirmMfaResponse {
4766
5476
  ok: boolean;
4767
5477
  }
4768
5478
 
5479
+ /** What the adapter's schema can actually answer about a version's dates. */
5480
+ interface PlanVersionMappingFields {
5481
+ /** `validFrom`/`validUntil` are maintained; otherwise both read as null. */
5482
+ validityWindows: boolean;
5483
+ /** `endsAt` exists; otherwise the field is left off the record entirely. */
5484
+ endsAt: boolean;
5485
+ }
5486
+ /** A `plans` row as either adapter reads it back. */
5487
+ interface CanonicalPlanRow {
5488
+ id: string;
5489
+ planKey: string;
5490
+ label: string;
5491
+ description: string | null;
5492
+ icon: string | null;
5493
+ sortOrder: number;
5494
+ createdAt: Date;
5495
+ updatedAt: Date;
5496
+ deletedAt: Date | null;
5497
+ }
5498
+ /** A `plan_versions` row as either adapter reads it back. */
5499
+ interface CanonicalPlanVersionRow {
5500
+ id: string;
5501
+ version: number;
5502
+ baseVersionId: string | null;
5503
+ features: unknown;
5504
+ quotas: unknown;
5505
+ monthlyNet: unknown;
5506
+ yearlyNet: unknown;
5507
+ marketed: boolean;
5508
+ publishedAt: Date | null;
5509
+ supersededAt: Date | null;
5510
+ publishedChanges: unknown;
5511
+ changeNote: string;
5512
+ nonRegressive: boolean;
5513
+ validFrom?: Date | null;
5514
+ validUntil?: Date | null;
5515
+ endsAt?: Date | null;
5516
+ createdByUserId: string | null;
5517
+ publishedByUserId: string | null;
5518
+ createdAt: Date;
5519
+ updatedAt: Date;
5520
+ }
5521
+ declare function toPlanRow(row: CanonicalPlanRow): PlanRow;
5522
+ /**
5523
+ * `planKey` is passed rather than read off the row: the canonical schema stores
5524
+ * the plan key in `planId`, but an adapter translating a consumer schema with a
5525
+ * real foreign key has to resolve it first, and only the adapter knows which
5526
+ * shape it is looking at.
5527
+ */
5528
+ declare function toPlanVersionRow(row: CanonicalPlanVersionRow, planKey: string, fields: PlanVersionMappingFields): PlanVersionRow;
5529
+
5530
+ /** A `subscription_contracts` row as either adapter reads it back. */
5531
+ interface CanonicalContractRow {
5532
+ id: string;
5533
+ tenantId: string;
5534
+ status: string;
5535
+ effectiveFrom: Date;
5536
+ effectiveUntil: Date | null;
5537
+ originalOfferId: string | null;
5538
+ originalPlanVersionId: string | null;
5539
+ originalBundleVersionIds: unknown;
5540
+ entitlementSnapshot: unknown;
5541
+ priceSnapshot: unknown;
5542
+ promotionSnapshots: unknown;
5543
+ promoCodeSnapshots: unknown;
5544
+ termsSnapshot: unknown;
5545
+ createdAt: Date;
5546
+ updatedAt: Date;
5547
+ }
5548
+ /** A `contract_line_items` row as either adapter reads it back. */
5549
+ interface CanonicalContractLineItemRow {
5550
+ id: string;
5551
+ contractId: string;
5552
+ kind: string;
5553
+ sourceKey: string;
5554
+ sourceVersionId: string | null;
5555
+ titleSnapshot: string;
5556
+ descriptionSnapshot: string | null;
5557
+ quantity: number;
5558
+ unit: string | null;
5559
+ priceNet: unknown;
5560
+ priceGross: unknown;
5561
+ billingCycle: string;
5562
+ currency: string;
5563
+ taxRate: unknown;
5564
+ taxAmount: unknown;
5565
+ minimumTermUntil: Date | null;
5566
+ featuresSnapshot: unknown;
5567
+ quotaEffectsSnapshot: unknown;
5568
+ metadata: unknown;
5569
+ createdAt: Date;
5570
+ }
5571
+ declare function toSubscriptionContractRecord(row: CanonicalContractRow, lineItems: CanonicalContractLineItemRow[]): SubscriptionContractRecord;
5572
+ declare function toContractLineItemRecord(row: CanonicalContractLineItemRow): ContractLineItemRecord;
5573
+
4769
5574
  /** Why a version is editable (for UI badges + audit logs). */
4770
5575
  type VersionEditableReason = 'draft' | 'pre-active';
4771
5576
  interface VersionEditability {
@@ -4780,6 +5585,30 @@ interface VersionEditability {
4780
5585
  */
4781
5586
  declare function isVersionEditable(v: VersionedEntityBase, now?: Date): VersionEditability;
4782
5587
 
5588
+ /** The part of a plan card this rule reads and writes. */
5589
+ interface RecommendablePlan {
5590
+ planKey: string;
5591
+ highlight: boolean;
5592
+ }
5593
+ /**
5594
+ * Leaves the mark on at most one plan, in place, and returns the winner.
5595
+ *
5596
+ * `inRequestedLocale` holds the keys of the plans whose card was described in
5597
+ * the language that was asked for. Where a caller has no fallback to model —
5598
+ * the SuperAdmin edits one language at a time — passing every key, or none,
5599
+ * gives the same answer: the first plan in the list order wins.
5600
+ *
5601
+ * The list order is the caller's, and it is what the reader sees, so the
5602
+ * answer is the first recommended card on the page. Said exactly, because it
5603
+ * is easy to overstate: the tie-break inherits whatever order the caller
5604
+ * arranged, and where two plans are equal by every criterion it sorted on,
5605
+ * that order is the repository's. `PlanRepository.list` promises none, so an
5606
+ * adapter that returns rows in a different order each time would move the mark
5607
+ * between two otherwise indistinguishable plans. The shipped adapters order
5608
+ * totally.
5609
+ */
5610
+ declare function keepOneRecommended<T extends RecommendablePlan>(plans: T[], inRequestedLocale: ReadonlySet<string>): T | null;
5611
+
4783
5612
  declare const ERROR_MESSAGES_EN: Record<PlatformErrorCode, string>;
4784
5613
  /** Values available for interpolation into a message template. */
4785
5614
  type ErrorMessageParams = Record<string, unknown>;
@@ -4789,6 +5618,23 @@ type ErrorMessageParams = Record<string, unknown>;
4789
5618
  * vanishing.
4790
5619
  */
4791
5620
  declare function formatErrorMessage(template: string, params?: ErrorMessageParams): string;
5621
+ /**
5622
+ * An error body this function can read.
5623
+ *
5624
+ * `code` is widened past `PlatformErrorCode` on purpose. The function checks
5625
+ * `typeof body.code === 'string'` and resolves whatever it finds, and the
5626
+ * `overrides` parameter exists so a consumer can bring its own codes — one
5627
+ * consumer carries 98 of them against the platform's 135, overlapping in five.
5628
+ * A closed union here would reject exactly the case the parameter is for, and
5629
+ * the cast that works around it is one a reader has to be told is deliberate.
5630
+ *
5631
+ * `PlatformErrorBody` stays closed: a body the *platform* produces really does
5632
+ * carry a platform code. It is the reader that has to accept more. The same
5633
+ * shape appears in `SaLocale` next door, for the same reason.
5634
+ */
5635
+ type ResolvableErrorBody = Omit<Partial<PlatformErrorBody>, 'code'> & Record<string, unknown> & {
5636
+ code?: PlatformErrorCode | (string & {});
5637
+ };
4792
5638
  /**
4793
5639
  * Turns an error body into display text.
4794
5640
  *
@@ -4802,8 +5648,8 @@ declare function formatErrorMessage(template: string, params?: ErrorMessageParam
4802
5648
  * second, so a template may name either without the value being duplicated on
4803
5649
  * the wire.
4804
5650
  */
4805
- declare function resolveErrorMessage(body: Partial<PlatformErrorBody> & Record<string, unknown>, overrides?: Partial<Record<string, string>>, defaults?: Partial<Record<string, string>>): string;
5651
+ declare function resolveErrorMessage(body: ResolvableErrorBody, overrides?: Partial<Record<string, string>>, defaults?: Partial<Record<string, string>>): string;
4806
5652
 
4807
5653
  declare const ERROR_MESSAGES_DE: Record<PlatformErrorCode, string>;
4808
5654
 
4809
- export { AUTH_ERROR_CODES, type ActionKey, type ActivationOrchestrator, type ActivePlanVersionWhere, type ActivePlanVersionWhereWithEndsAt, type ActiveVersionWhere, type ActiveVersionWhereWithEndsAt, type ActorTag, type AdminActor, type AdminAuditListFilter, type AdminManifest, type AdminResourcesPort, type AdminSubscriptionListRow, type AdminTenantDetail, type AdminTenantListFilter, type AdminTenantListRow, type AdminTenantStateResult, type AdminUserListFilter, type AdminUserListRow, type ApplyOnboardingSelectionInput, type ApplyOnboardingSelectionResult, type ApprovedCatalogKeys, type AuditActionDef, type AuditEntry, type AuditPort, type AuditQuery, type AuditQueryPort, type AuditStatsPort, type AuditStatsSnapshot, type AuthErrorCode, BILLING_ERROR_CODES, type BillingCycle, type BillingErrorCode, type BundleAvailabilityState, type BundleCompatibility, type BundleFeatureShape, type BundleListFilter, type BundlePricingOverride, type BundleRepository, type BundleRow, type BundleVersionFields, type BundleVersionMutationResult, type BundleVersionRow, CATALOG_ERROR_CODES, CONTRACT_ERROR_CODES, type CancelSubscriptionBundleData, type CapabilityCatalogEntryRow, type CapabilityCodeStatus, type CapabilityKey, type CapabilityKind, type CatalogEntryFilter, type CatalogEntryI18n, type CatalogEntryI18nFields, type CatalogEntryRepository, type CatalogErrorCode, type ChangeDirection, type CheckoutOfferFilter, type CheckoutOfferLineItem, type CheckoutOfferLineItemKind, type CheckoutOfferPriceBreakdown, type CheckoutOfferPromoCodeSnapshot, type CheckoutOfferPromotionSnapshot, type CheckoutOfferRepository, type CheckoutOfferRow, type CheckoutOfferStatus, type CheckoutSession, type CleanupResult, type CliUserRow, type ComponentKey, type ConfiguratorCatalog, type ConfiguratorMarketingProvider, type ConfiguratorModel, type ConfiguratorPlanMarketing, type ConfiguratorPlanVersionRow, type ConfiguratorPriceBreakdown, type ConfiguratorSourcesLookup, type ContractErrorCode, type ContractLineItemKind, type ContractLineItemRecord, type CreateBundleData, type CreateBundleVersionDraftData, type CreateCheckoutOfferData, type CreateMarketingProjectionData, type CreatePlanData, type CreatePlanVersionDraftData, type CreatePromoCodeData, type CreatePromoCodeRequest, type CreatePromotionData, type CreateSubscriptionBundleData, type CreateSubscriptionContractData, type CreateSuperAdminCliInput, type CreateTenantInput, type DiffResult, type DiscoveredCapability, type DiscoveredFeature, type DiscoveredQuota, type DiscoveredQuotaPolicy, type DiscoveryCodeStatus, type DiscoverySnapshot, type DiscoveryStatus, ERROR_MESSAGES_DE, ERROR_MESSAGES_EN, type EffectiveLimitsSnapshot, type ErrorMessageParams, FEATURE_NOT_LICENSED, type FeatureCatalogEntryRow, type FeatureDef, type FeatureKey, type FeatureNotLicensedBody, type FeatureRequiresIndex, type FeatureTier, type FeatureUiMeta, type FeatureUiRegistry, type FinalActivationResult, type FirstTimeCustomerCheck, type HandlePaymentEventInput, type HandlePaymentEventReason, type HandlePaymentEventResult, type ImmediatePlanChangeInput, type InvoiceLineItemSnapshot, type KpiCardDef, type KpiDisplayHint, type ManifestAccessPort, type ManifestContribution, type MarketingProjectionFilter, type MarketingProjectionRepository, type MarketingProjectionRow, type MarketingSettingsRepository, type MarketingSettingsRow, type MarketingTargetType, type MarketingTopFeature, type MfaPort, type NewContractLineItemData, OTP_RATE_LIMIT_MAX_SENDS, OTP_RATE_LIMIT_WINDOW_MINUTES, OTP_TTL_MINUTES, OTP_VERIFY_MAX_ATTEMPTS, type OnboardingPromoRedemption, type OnboardingSelectionRequest, type OnboardingSelectionResponse, PASSWORD_RESET_TTL_MINUTES, PENDING_CHECKOUT_TTL_DAYS, PENDING_EMAIL_TTL_HOURS, PENDING_ONBOARDING_TTL_DAYS, PLATFORM_ERROR_CODES, PROMO_ERROR_CODES, type Paginated, type PasswordHasher, type PasswordResetCliResult, type PaymentEventLog, type PaymentEventStatus, type PaymentProvider, type PendingRegistration, type PendingRegistrationCreateInput, type PendingRegistrationRepository, type PendingRegistrationSnapshot, type PendingRegistrationUpdateInput, type PersistenceCapabilities, PersistenceCapabilityError, type PersistenceClassRef, type PersistenceInjectionToken, type PersistenceProvider, type PlanCatalog, type PlanCatalogApp, type PlanCatalogImportReport, type PlanCatalogImportSink, type PlanCatalogLookup, type PlanCatalogMarketing, type PlanCatalogReadSink, type PlanCatalogReadSnapshot, type PlanDef, type PlanId, type PlanListFilter, type PlanRepository, type PlanRow, type PlanVersion, type PlanVersionFields, type PlanVersionMutationResult, type PlanVersionRecord, type PlanVersionRepository, type PlanVersionRow, type PlatformErrorBody, type PlatformErrorCode, type PlatformRole, type PlatformUserDto, PlatformUserExistsError, type ProjectPageDef, type PromoCode, type PromoCodeDurationType, type PromoCodeFilter, type PromoCodeRecord, type PromoCodeRedemption, type PromoCodeRedemptionListItem, type PromoCodeRedemptionRecord, type PromoCodeRedemptionRepository, type PromoCodeRedemptionStatus, type PromoCodeRepository, type PromoCodeStatsPort, type PromoCodeStatsSnapshot, type PromoCodeStatus, type PromoCodeValidationLog, type PromoCodeValidationLogRepository, type PromoCodeValidationResult, type PromoCodeValueType, type PromoErrorCode, type PromoPreviewInvalidReason, type PromoPreviewRequest, type PromoPreviewResponse, type PromoPreviewValidResponse, type PromoRevenueDeductionAggregator, type PromoSubscriptionLookup, type PromotionBillingCycle, type PromotionFilter, type PromotionI18n, type PromotionI18nFields, type PromotionRepository, type PromotionResult, type PromotionRow, type PromotionStatus, type PromotionTargetType, type PromotionType, type PromotionValue, type PublicBootResponse, type PublicComparisonRow, type PublicMarketingBundle, type PublicMarketingCatalogResponse, type PublicMarketingPlan, type PublicMarketingPromo, type PublicSignupPlan, type PublishBundleVersionData, type PublishPlanVersionData, type QuotaCatalogEntryRow, type QuotaEnforcementMode, type QuotaKey, type QuotaProvider, REGISTRATION_ERROR_CODES, REGISTRATION_RESUME_TTL_MINUTES, REGISTRATION_STEP_BY_STATUS, type ReassignTenantAdminCliResult, type RedeemPromoInTransactionCallback, type RegistrationAuditContext, type RegistrationAuditEvent, type RegistrationAuditEventType, type RegistrationAuditLogger, type RegistrationConfigSelection, type RegistrationConfiguratorLookup, type RegistrationErrorCode, type RegistrationOtpDelivery, type RegistrationPromoPreview, type RegistrationResumeDelivery, type RegistrationResumeTokenSigner, type RegistrationStatus, type RegistrationStep, type RequiredCapabilities, type ResumeRegistrationInput, type ResumeRegistrationResult, type ReviewCatalogEntryData, type RlsBypassPort, SETUP_ERROR_CODES, type SaaSiCatPersistenceAdapter, type SaaSiCatPersistenceAdminResources, type SaaSiCatPersistenceCatalog, type SaaSiCatPersistenceCore, type SaaSiCatPersistenceEntitlement, type SaaSiCatPersistencePromo, type SaaSiCatPersistenceTenantBilling, type SaveRegistrationConfigInput, type SaveRegistrationConfigResult, type ScheduledPlanChangeInput, type SelectPlanInput, type SelectPlanResult, type SelectableBundleShape, type SetCatalogEntryReviewData, type SetupConfirmMfaRequest, type SetupConfirmMfaResponse, type SetupErrorCode, type SetupRequest, type SetupResult, type SetupStatusResponse, type SlugAvailabilityCheck, type StandardPageDef, type StandardPageKey, type StartCheckoutInput, type StartCheckoutResult, type StartRegistrationInput, type StartRegistrationResult, type StrictModeWarning, type StrictModeWarningCode, type Subscription, type SubscriptionBundleRecord, type SubscriptionBundleRepository, type SubscriptionBundleView, type SubscriptionContractFilter, type SubscriptionContractInvoiceSnapshot, type SubscriptionContractPriceSnapshot, type SubscriptionContractRecord, type SubscriptionContractRepository, type SubscriptionContractStatus, type SubscriptionRecord, type SubscriptionRepository, type SubscriptionStatsPort, type SubscriptionStatsSnapshot, type SubscriptionStatus, type SubscriptionUsagePort, type SubscriptionUsageRecord, type SuperAdminProvisioningPort, type SyncDiscoveryResult, type TenantActionDef, type TenantColumnDef, type TenantDto, type TenantListFilter, type TenantPort, type TenantSubscriptionWritePort, type TerminateSubscriptionContractData, type TopPromoCode, type TransactionContext, type TransactionRunner, type UpdateBundleData, type UpdateBundleVersionDraftData, type UpdateCatalogEntryBaseData, type UpdateCatalogEntryI18nData, type UpdateCheckoutOfferData, type UpdateMarketingProjectionData, type UpdateMarketingSettingsData, type UpdatePlanData, type UpdatePlanVersionDraftData, type UpdatePromoCodeData, type UpdatePromoCodeRequest, type UpdatePromotionData, type UpsellOffer, type UpsellOfferResolver, type UpsertCapabilityEntryData, type UpsertFeatureCatalogEntryInput, type UpsertFeatureEntryData, type UpsertPlanInput, type UpsertPlanVersionInput, type UpsertQuotaEntryData, type UpsertResult, type UsageSnapshotPort, type UserAccountLookup, type UserListFilter, type UserManagementPort, type UserPort, type VerifyRegistrationOtpResult, type VersionChange, type VersionChangeDirection, type VersionEditability, type VersionEditableReason, type VersionedEntityBase, applyPromo, assertPersistenceCapabilities, buildActivePlanVersionWhere, buildActiveVersionWhere, buildFeatureRequiresIndex, classifyBundleVersionDiff, classifyPlanDiff, collectUnsatisfiedRequires, coverageExcludingSelf, formatErrorMessage, isBundleRedundant, isPlatformUserExistsError, isVersionEditable, missingRequiresFor, pickActivePromo, promoStatus, resolveBundleAvailability, resolveErrorMessage, selectChargeableBundles, startOfUtcDay };
5655
+ export { ACTIVE_SUBSCRIPTION_CONTRACT_STATUSES, AUTH_ERROR_CODES, type ActionKey, type ActivationOrchestrator, type ActivePlanVersionWhere, type ActivePlanVersionWhereWithEndsAt, type ActiveVersionWhere, type ActiveVersionWhereWithEndsAt, type ActorTag, type AdminActor, type AdminAuditListFilter, type AdminManifest, type AdminResourcesPort, type AdminSubscriptionListRow, type AdminTenantDetail, type AdminTenantListFilter, type AdminTenantListRow, type AdminTenantStateResult, type AdminUserListFilter, type AdminUserListRow, type AppliedSettingsPort, type AppliedSettingsRecord, type AppliedSettingsValues, type ApplyOnboardingSelectionInput, type ApplyOnboardingSelectionResult, type ApprovedCatalogKeys, type AuditActionDef, type AuditEntry, type AuditPort, type AuditQuery, type AuditQueryPort, type AuditStatsPort, type AuditStatsSnapshot, type AuthErrorCode, BILLING_ERROR_CODES, BUNDLE_PRICE_LOOKUP_LIMIT, type BillingCycle, type BillingErrorCode, type BundleAvailabilityState, type BundleCompatibility, type BundleFeatureShape, type BundleListFilter, type BundlePricingOverride, type BundleRepository, type BundleRow, type BundleVersionFields, type BundleVersionMutationResult, type BundleVersionRow, CATALOGUE_KEYS, CATALOG_ERROR_CODES, CONTRACT_ERROR_CODES, type CancelSubscriptionBundleData, type CancelSubscriptionInput, type CancelSubscriptionResult, type CancellationNoticePeriods, type CanonicalContractLineItemRow, type CanonicalContractRow, type CanonicalPlanRow, type CanonicalPlanVersionRow, type CapabilityCatalogEntryRow, type CapabilityCodeStatus, type CapabilityKey, type CapabilityKind, type CatalogEntryFilter, type CatalogEntryI18n, type CatalogEntryI18nFields, type CatalogEntryRepository, type CatalogErrorCode, type ChangeDirection, type CheckoutOfferFilter, type CheckoutOfferLineItem, type CheckoutOfferLineItemKind, type CheckoutOfferPriceBreakdown, type CheckoutOfferPromoCodeSnapshot, type CheckoutOfferPromotionSnapshot, type CheckoutOfferRepository, type CheckoutOfferRow, type CheckoutOfferStatus, type CheckoutSession, type CleanupResult, type CliUserRow, type ComponentKey, type ConfiguratorCatalog, type ConfiguratorMarketingProvider, type ConfiguratorModel, type ConfiguratorPlanMarketing, type ConfiguratorPlanVersionRow, type ConfiguratorPriceBreakdown, type ConfiguratorSourcesLookup, type ContractErrorCode, type ContractLineItemKind, type ContractLineItemRecord, type CreateBundleData, type CreateBundleVersionDraftData, type CreateCheckoutOfferData, type CreateMarketingProjectionData, type CreatePlanData, type CreatePlanVersionDraftData, type CreatePromoCodeData, type CreatePromoCodeRequest, type CreatePromotionData, type CreateSubscriptionBundleData, type CreateSubscriptionContractData, type CreateSuperAdminCliInput, type CreateTenantInput, type DiffResult, type DiscoveredCapability, type DiscoveredFeature, type DiscoveredQuota, type DiscoveredQuotaPolicy, type DiscoveryCodeStatus, type DiscoverySnapshot, type DiscoveryStatus, ERROR_MESSAGES_DE, ERROR_MESSAGES_EN, type EffectiveLimitsSnapshot, type EmailPort, type ErrorMessageParams, FEATURE_NOT_LICENSED, type FeatureCatalogEntryRow, type FeatureDef, type FeatureKey, type FeatureNotLicensedBody, type FeatureRequiresIndex, type FeatureTier, type FeatureUiMeta, type FeatureUiRegistry, type FinalActivationResult, type FirstTimeCustomerCheck, type HandlePaymentEventInput, type HandlePaymentEventReason, type HandlePaymentEventResult, type ImmediatePlanChangeInput, type InvoiceLineItemSnapshot, type KpiCardDef, type KpiDisplayHint, MARKETING_PRIORITY_MAX, MARKETING_PRIORITY_MIN, type ManifestAccessPort, type ManifestContribution, type MarketingProjectionFilter, type MarketingProjectionRepository, type MarketingProjectionRow, type MarketingSettingsRepository, type MarketingSettingsRow, type MarketingTargetType, type MarketingTopFeature, type MfaPort, type NewContractLineItemData, type NewSettingsChange, OTP_RATE_LIMIT_MAX_SENDS, OTP_RATE_LIMIT_WINDOW_MINUTES, OTP_TTL_MINUTES, OTP_VERIFY_MAX_ATTEMPTS, type OnboardingPromoRedemption, type OnboardingSelectionRequest, type OnboardingSelectionResponse, PASSWORD_RESET_TTL_MINUTES, PENDING_CHECKOUT_TTL_DAYS, PENDING_EMAIL_TTL_HOURS, PENDING_ONBOARDING_TTL_DAYS, PLATFORM_ERROR_CODES, PROMO_ERROR_CODES, type Paginated, type PasswordHasher, type PasswordResetCliResult, type PaymentEventLog, type PaymentEventStatus, type PaymentProvider, type PendingRegistration, type PendingRegistrationCreateInput, type PendingRegistrationRepository, type PendingRegistrationSnapshot, type PendingRegistrationUpdateInput, type PersistenceCapabilities, PersistenceCapabilityError, type PersistenceClassRef, type PersistenceInjectionToken, type PersistenceProvider, type PlanCatalog, type PlanCatalogApp, type PlanCatalogImportReport, type PlanCatalogImportSink, type PlanCatalogLookup, type PlanCatalogMarketing, type PlanCatalogNotifications, type PlanCatalogReadSink, type PlanCatalogReadSnapshot, type PlanCatalogTenantBilling, type PlanDef, type PlanId, type PlanListFilter, type PlanRepository, type PlanRow, type PlanVersion, type PlanVersionFields, type PlanVersionMappingFields, type PlanVersionMutationResult, type PlanVersionRecord, type PlanVersionRepository, type PlanVersionRow, type PlatformErrorBody, type PlatformErrorCode, type PlatformRole, type PlatformUserDto, PlatformUserExistsError, type ProjectPageDef, type PromoCode, type PromoCodeDurationType, type PromoCodeFilter, type PromoCodeRecord, type PromoCodeRedemption, type PromoCodeRedemptionListItem, type PromoCodeRedemptionRecord, type PromoCodeRedemptionRepository, type PromoCodeRedemptionStatus, type PromoCodeRepository, type PromoCodeStatsPort, type PromoCodeStatsSnapshot, type PromoCodeStatus, type PromoCodeValidationLog, type PromoCodeValidationLogRepository, type PromoCodeValidationResult, type PromoCodeValueType, type PromoErrorCode, type PromoPreviewInvalidReason, type PromoPreviewRequest, type PromoPreviewResponse, type PromoPreviewValidResponse, type PromoRevenueDeductionAggregator, type PromoSubscriptionLookup, type PromotionBillingCycle, type PromotionI18n, type PromotionI18nFields, type PromotionRepository, type PromotionResult, type PromotionRow, type PromotionStatus, type PromotionTargetType, type PromotionType, type PromotionValue, type PublicBootResponse, type PublicComparisonRow, type PublicMarketingBundle, type PublicMarketingCatalogResponse, type PublicMarketingPlan, type PublicMarketingPromo, type PublicSignupPlan, type PublishBundleVersionData, type PublishBundleVersionMeta, type PublishPlanVersionData, type QuotaCatalogEntryRow, type QuotaEnforcementMode, type QuotaKey, type QuotaProvider, REGISTRATION_ERROR_CODES, REGISTRATION_RESUME_TTL_MINUTES, REGISTRATION_STEP_BY_STATUS, type ReassignTenantAdminCliResult, type RecommendablePlan, type RedeemPromoInTransactionCallback, type RegistrationAuditContext, type RegistrationAuditEvent, type RegistrationAuditEventType, type RegistrationAuditLogger, type RegistrationConfigSelection, type RegistrationConfiguratorLookup, type RegistrationErrorCode, type RegistrationOtpDelivery, type RegistrationPromoPreview, type RegistrationResumeDelivery, type RegistrationResumeTokenSigner, type RegistrationStatus, type RegistrationStep, type RequiredCapabilities, type ResolvableErrorBody, type ResumeRegistrationInput, type ResumeRegistrationResult, type ReviewCatalogEntryData, type RlsBypassPort, SETTINGS_ERROR_CODES, SETUP_ERROR_CODES, type SaaSiCatPersistenceAdapter, type SaaSiCatPersistenceAdminResources, type SaaSiCatPersistenceCatalog, type SaaSiCatPersistenceCore, type SaaSiCatPersistenceEntitlement, type SaaSiCatPersistencePromo, type SaaSiCatPersistenceTenantBilling, type SaveRegistrationConfigInput, type SaveRegistrationConfigResult, type ScheduledPlanChangeInput, type SelectPlanInput, type SelectPlanResult, type SelectableBundleShape, type SelfServiceBlockedPlans, type SetCatalogEntryReviewData, type SettingsChangeFilter, type SettingsChangeRecord, type SettingsDifference, type SettingsErrorCode, type SetupConfirmMfaRequest, type SetupConfirmMfaResponse, type SetupErrorCode, type SetupRequest, type SetupResult, type SetupStatusResponse, type SlugAvailabilityCheck, type StandardPageDef, type StandardPageKey, type StartCheckoutInput, type StartCheckoutResult, type StartRegistrationInput, type StartRegistrationResult, type StoredBundleStem, type StrictModeWarning, type StrictModeWarningCode, type Subscription, type SubscriptionBundleRecord, type SubscriptionBundleRepository, type SubscriptionBundleView, type SubscriptionContractFilter, type SubscriptionContractInvoiceSnapshot, type SubscriptionContractPriceSnapshot, type SubscriptionContractRecord, type SubscriptionContractRepository, type SubscriptionContractStatus, type SubscriptionRecord, type SubscriptionRepository, type SubscriptionStatsPort, type SubscriptionStatsSnapshot, type SubscriptionStatus, type SubscriptionUsagePort, type SubscriptionUsageRecord, type SuperAdminProvisioningPort, type SyncDiscoveryResult, type TenantActionDef, type TenantColumnDef, type TenantDto, type TenantListFilter, type TenantPort, type TenantSubscriptionWritePort, type TerminateSubscriptionContractData, type TopPromoCode, type TransactionContext, type TransactionRunner, type UpdateBundleData, type UpdateBundleVersionDraftData, type UpdateCatalogEntryBaseData, type UpdateCatalogEntryI18nData, type UpdateCheckoutOfferData, type UpdateMarketingProjectionData, type UpdateMarketingSettingsData, type UpdatePlanData, type UpdatePlanVersionDraftData, type UpdatePromoCodeData, type UpdatePromoCodeRequest, type UpdatePromotionData, type UpsellOffer, type UpsellOfferResolver, type UpsertCapabilityEntryData, type UpsertFeatureCatalogEntryInput, type UpsertFeatureEntryData, type UpsertPlanInput, type UpsertPlanVersionInput, type UpsertQuotaEntryData, type UpsertResult, type UsageSnapshotPort, type UserAccountLookup, type UserListFilter, type UserManagementPort, type UserPort, type VerifyRegistrationOtpResult, type VersionChange, type VersionChangeDirection, type VersionEditability, type VersionEditableReason, type VersionedEntityBase, applyPromo, assertPersistenceCapabilities, buildActivePlanVersionWhere, buildActiveVersionWhere, buildFeatureRequiresIndex, bundleDraftDefaults, bundleStemDefaults, canonicalJson, classifyBundleVersionDiff, classifyPlanDiff, collectUnsatisfiedRequires, coverageExcludingSelf, definedFields, diffSettings, formatErrorMessage, isBundleRedundant, isPlatformUserExistsError, isVersionEditable, keepOneRecommended, missingRequiresFor, pickActivePromo, previousUtcDay, promoStatus, readQuotaRecord, readQuotaValue, resolveBundleAvailability, resolveErrorMessage, selectChargeableBundles, settingsSubtreeOf, startOfUtcDay, toBundleStemRow, toContractLineItemRecord, toPlanRow, toPlanVersionRow, toSubscriptionContractRecord };