@saasicat/core 1.0.0-rc.2 → 1.0.0-rc.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -0
- package/dist/.build-stamp +1 -1
- package/dist/index.cjs +1017 -50
- package/dist/index.d.cts +2721 -477
- package/dist/index.d.ts +2721 -477
- package/dist/index.js +970 -49
- package/package.json +2 -1
package/dist/index.d.cts
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,15 +437,13 @@ interface MarketingProjectionRow {
|
|
|
668
437
|
createdAt: string;
|
|
669
438
|
updatedAt: string;
|
|
670
439
|
}
|
|
671
|
-
/** Filter for `MarketingProjectionRepository.list()`.
|
|
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
446
|
interface CreateMarketingProjectionData {
|
|
679
|
-
projectKey: string;
|
|
680
447
|
targetType: MarketingTargetType;
|
|
681
448
|
targetVersionId: string;
|
|
682
449
|
locale?: string;
|
|
@@ -706,6 +473,409 @@ interface UpdateMarketingProjectionData {
|
|
|
706
473
|
highlight?: boolean;
|
|
707
474
|
}
|
|
708
475
|
|
|
476
|
+
/** The address lines a party is named with. Every one of them may be unknown. */
|
|
477
|
+
interface PartyAddress {
|
|
478
|
+
/** Street and number. */
|
|
479
|
+
addressLine1: string | null;
|
|
480
|
+
/** A second line, such as a building or a c/o. */
|
|
481
|
+
addressLine2: string | null;
|
|
482
|
+
postalCode: string | null;
|
|
483
|
+
city: string | null;
|
|
484
|
+
/** ISO 3166-1 alpha-2, upper case. */
|
|
485
|
+
country: string | null;
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* What names a legal entity: the name it is registered under and the
|
|
489
|
+
* identifiers its tax office gave it.
|
|
490
|
+
*
|
|
491
|
+
* These are the party a contract was concluded with. Under a running contract
|
|
492
|
+
* they change only as a correction of that same entity, recorded with the
|
|
493
|
+
* values they replaced and the reason; contact details change freely.
|
|
494
|
+
*/
|
|
495
|
+
interface LegalIdentity {
|
|
496
|
+
/** The registered name, legal form included. */
|
|
497
|
+
legalName: string;
|
|
498
|
+
/** VAT identification number. */
|
|
499
|
+
vatId: string | null;
|
|
500
|
+
/** The national tax number, where one is stated beside or instead of the VAT id. */
|
|
501
|
+
taxNumber: string | null;
|
|
502
|
+
}
|
|
503
|
+
/** The fields of the legal identity, the ones a correction may change. */
|
|
504
|
+
type LegalIdentityField = keyof LegalIdentity;
|
|
505
|
+
/** Those fields, in the order they are named and shown. */
|
|
506
|
+
declare const LEGAL_IDENTITY_FIELDS: readonly LegalIdentityField[];
|
|
507
|
+
/** Whether two identities name the same entity: every field, value for value. */
|
|
508
|
+
declare function sameLegalIdentity(before: LegalIdentity, after: LegalIdentity): boolean;
|
|
509
|
+
/** The fields whose value differs between two identities, in field order. */
|
|
510
|
+
declare function movedIdentityFields(before: LegalIdentity, after: LegalIdentity): LegalIdentityField[];
|
|
511
|
+
|
|
512
|
+
/** The payment methods SaaSiCat takes, by the names `config/saas.yaml` uses. */
|
|
513
|
+
type PaymentMethodType = 'card' | 'sepa_debit';
|
|
514
|
+
/**
|
|
515
|
+
* Whom a payment method is set up for. The gateway hands it back unchanged
|
|
516
|
+
* with its confirmation, which is how the confirmation finds its way to the
|
|
517
|
+
* sign-up or the subscriber that asked for it.
|
|
518
|
+
*/
|
|
519
|
+
type PaymentMethodSetupSubject = {
|
|
520
|
+
kind: 'registration';
|
|
521
|
+
pendingRegistrationId: string;
|
|
522
|
+
} | {
|
|
523
|
+
kind: 'subscriber';
|
|
524
|
+
subscriberId: string;
|
|
525
|
+
};
|
|
526
|
+
/** The party the gateway keeps the payment method for: its customer there. */
|
|
527
|
+
interface PaymentMethodHolder {
|
|
528
|
+
/** The legal name, which a direct debit mandate names. */
|
|
529
|
+
name: string;
|
|
530
|
+
/** Where the gateway sends what it sends; `null` lets its form ask. */
|
|
531
|
+
email: string | null;
|
|
532
|
+
address: PartyAddress;
|
|
533
|
+
/**
|
|
534
|
+
* The customer this party already has at the account, from an earlier
|
|
535
|
+
* payment method. `null` asks the gateway for a new one.
|
|
536
|
+
*/
|
|
537
|
+
customerRef: string | null;
|
|
538
|
+
}
|
|
539
|
+
interface StartPaymentMethodSetupInput {
|
|
540
|
+
subject: PaymentMethodSetupSubject;
|
|
541
|
+
holder: PaymentMethodHolder;
|
|
542
|
+
/** What the form offers, from the account's `methods`. */
|
|
543
|
+
methods: readonly PaymentMethodType[];
|
|
544
|
+
/** Where the gateway sends the person once the form is done. */
|
|
545
|
+
successUrl: string;
|
|
546
|
+
/** Where the gateway sends the person who leaves the form. */
|
|
547
|
+
cancelUrl: string;
|
|
548
|
+
}
|
|
549
|
+
/** A body and headers exactly as they arrived, before anything parsed them. */
|
|
550
|
+
interface PaymentGatewayCallback {
|
|
551
|
+
body: string | Uint8Array;
|
|
552
|
+
headers: Readonly<Record<string, string | readonly string[] | undefined>>;
|
|
553
|
+
}
|
|
554
|
+
interface PaymentMethodSetupSession {
|
|
555
|
+
/**
|
|
556
|
+
* The gateway's identifier of the session, unique within its account — and
|
|
557
|
+
* an adapter that has no session of its own makes one per start rather than
|
|
558
|
+
* a constant. A session is confirmed once, which the event log holds, so a
|
|
559
|
+
* value two starts share lets the second confirmation through as the
|
|
560
|
+
* duplicate it is not.
|
|
561
|
+
*/
|
|
562
|
+
sessionRef: string;
|
|
563
|
+
/** The gateway's form, where the person is sent next. */
|
|
564
|
+
redirectUrl: string;
|
|
565
|
+
/** The customer the payment method is set up for, created if none was given. */
|
|
566
|
+
customerRef: string;
|
|
567
|
+
/**
|
|
568
|
+
* The last moment a confirmation of this session can still arrive: the end
|
|
569
|
+
* of its form, after which nobody can complete it, plus the time the
|
|
570
|
+
* gateway goes on retrying a confirmation it could not deliver. `null` for
|
|
571
|
+
* a gateway that states neither. A sign-up's promo code slot is held until
|
|
572
|
+
* then, so a form paid shortly before its end still finds its slot when the
|
|
573
|
+
* confirmation arrives late, and an abandoned form gives it back.
|
|
574
|
+
*/
|
|
575
|
+
confirmableUntil: Date | null;
|
|
576
|
+
/**
|
|
577
|
+
* A confirmation the gateway already holds when the session starts, to be
|
|
578
|
+
* handled like any callback. Only a gateway without a form of its own has
|
|
579
|
+
* one — the development gateway; a real gateway confirms through its
|
|
580
|
+
* webhook.
|
|
581
|
+
*/
|
|
582
|
+
immediateCallback?: PaymentGatewayCallback;
|
|
583
|
+
}
|
|
584
|
+
/**
|
|
585
|
+
* What SaaSiCat keeps about a payment method: enough to show which one it is,
|
|
586
|
+
* never enough to pay with it (`SC-PRIV-005`).
|
|
587
|
+
*/
|
|
588
|
+
interface MaskedPaymentMethod {
|
|
589
|
+
type: PaymentMethodType;
|
|
590
|
+
/** The card network, such as `visa`; `null` for a direct debit. */
|
|
591
|
+
brand: string | null;
|
|
592
|
+
/** The last four digits of the card number or the IBAN. */
|
|
593
|
+
last4: string;
|
|
594
|
+
/** 1–12; `null` for a direct debit. */
|
|
595
|
+
expiryMonth: number | null;
|
|
596
|
+
/** Four digits; `null` for a direct debit. */
|
|
597
|
+
expiryYear: number | null;
|
|
598
|
+
/** ISO 3166-1 alpha-2 of the card's issuer or the bank account, where the gateway says. */
|
|
599
|
+
country: string | null;
|
|
600
|
+
/** The bank code of a direct debit account, where the gateway says. */
|
|
601
|
+
bankCode: string | null;
|
|
602
|
+
/** The reference of a direct debit mandate, which a debit announcement quotes. */
|
|
603
|
+
mandateReference: string | null;
|
|
604
|
+
}
|
|
605
|
+
/** A payment method the gateway confirmed, with the references that reach it there. */
|
|
606
|
+
interface ConfirmedPaymentMethod extends MaskedPaymentMethod {
|
|
607
|
+
customerRef: string;
|
|
608
|
+
paymentMethodRef: string;
|
|
609
|
+
}
|
|
610
|
+
/** A gateway callback, verified and translated. */
|
|
611
|
+
type PaymentGatewayEvent = {
|
|
612
|
+
kind: 'payment-method-confirmed';
|
|
613
|
+
/** The gateway's identifier of the event, unique within its account. */
|
|
614
|
+
eventId: string;
|
|
615
|
+
/** When the gateway says it happened. */
|
|
616
|
+
occurredAt: Date;
|
|
617
|
+
sessionRef: string;
|
|
618
|
+
subject: PaymentMethodSetupSubject;
|
|
619
|
+
paymentMethod: ConfirmedPaymentMethod;
|
|
620
|
+
} | {
|
|
621
|
+
kind: 'payment-method-setup-failed';
|
|
622
|
+
eventId: string;
|
|
623
|
+
occurredAt: Date;
|
|
624
|
+
sessionRef: string;
|
|
625
|
+
subject: PaymentMethodSetupSubject;
|
|
626
|
+
} | {
|
|
627
|
+
/** Genuine, and nothing SaaSiCat acts on. */
|
|
628
|
+
kind: 'unhandled';
|
|
629
|
+
eventId: string;
|
|
630
|
+
occurredAt: Date;
|
|
631
|
+
/** The gateway's own name for the event, for the log. */
|
|
632
|
+
type: string;
|
|
633
|
+
};
|
|
634
|
+
/**
|
|
635
|
+
* One account at a payment gateway: its keys, its form and its callbacks.
|
|
636
|
+
*
|
|
637
|
+
* An adapter is bound to one account, and `config/saas.yaml#payments.accounts`
|
|
638
|
+
* names it. Mollie or any other provider is another adapter behind this port.
|
|
639
|
+
*/
|
|
640
|
+
interface PaymentGateway {
|
|
641
|
+
/** The provider, as `config/saas.yaml` names it, e.g. `stripe`. */
|
|
642
|
+
readonly provider: string;
|
|
643
|
+
/**
|
|
644
|
+
* Opens the gateway's form for a payment method. Nothing is confirmed until
|
|
645
|
+
* the gateway says so through `readCallback`.
|
|
646
|
+
*/
|
|
647
|
+
startPaymentMethodSetup(input: StartPaymentMethodSetupInput): Promise<PaymentMethodSetupSession>;
|
|
648
|
+
/**
|
|
649
|
+
* Verifies a callback with the account's secret and translates it.
|
|
650
|
+
* Throws `PaymentCallbackRejectedError` for anything the gateway did not
|
|
651
|
+
* send, before a single field of it is trusted.
|
|
652
|
+
*/
|
|
653
|
+
readCallback(callback: PaymentGatewayCallback): Promise<PaymentGatewayEvent>;
|
|
654
|
+
}
|
|
655
|
+
/**
|
|
656
|
+
* A callback that is not the gateway's: a missing or wrong signature, a stale
|
|
657
|
+
* timestamp, a body that was altered. Nothing is read from it.
|
|
658
|
+
*/
|
|
659
|
+
declare class PaymentCallbackRejectedError extends Error {
|
|
660
|
+
readonly code = "PAYMENT_CALLBACK_REJECTED";
|
|
661
|
+
constructor(reason: string);
|
|
662
|
+
}
|
|
663
|
+
/** Realm-safe type guard, like `isPlatformUserExistsError`. */
|
|
664
|
+
declare function isPaymentCallbackRejectedError(err: unknown): err is PaymentCallbackRejectedError;
|
|
665
|
+
|
|
666
|
+
type FeatureKey = string;
|
|
667
|
+
type PlanId = string;
|
|
668
|
+
type QuotaKey = string;
|
|
669
|
+
interface FeatureDef {
|
|
670
|
+
key: FeatureKey;
|
|
671
|
+
label?: string;
|
|
672
|
+
icon?: string;
|
|
673
|
+
/** CORE / ADVANCED / PRO / BUSINESS / ENTERPRISE_ONLY — convention. */
|
|
674
|
+
tier?: string;
|
|
675
|
+
plannedOnly?: boolean;
|
|
676
|
+
}
|
|
677
|
+
interface PlanDef {
|
|
678
|
+
id: PlanId;
|
|
679
|
+
name?: string;
|
|
680
|
+
tagline?: string;
|
|
681
|
+
/** false = not selectable in self-service onboarding. Default: true. */
|
|
682
|
+
marketed?: boolean;
|
|
683
|
+
/** Highlighted card in onboarding (max. 1 per catalog). */
|
|
684
|
+
popular?: boolean;
|
|
685
|
+
/** Net monthly price. null = on request. */
|
|
686
|
+
monthlyNet?: number | null;
|
|
687
|
+
/** Net total amount per year. null = monthly only. */
|
|
688
|
+
yearlyNet?: number | null;
|
|
689
|
+
/** Map quotaKey → max value. -1 = unlimited. */
|
|
690
|
+
quotas: Record<QuotaKey, number>;
|
|
691
|
+
features: FeatureKey[];
|
|
692
|
+
}
|
|
693
|
+
/** App-wide marketing configuration. */
|
|
694
|
+
interface PlanCatalogMarketing {
|
|
695
|
+
/**
|
|
696
|
+
* Allowed language pool that the app may market. First = default
|
|
697
|
+
* locale. From it, the SuperAdmin activates a subset in the marketing
|
|
698
|
+
* catalog (LocaleManager).
|
|
699
|
+
*/
|
|
700
|
+
availableLocales: string[];
|
|
701
|
+
}
|
|
702
|
+
/**
|
|
703
|
+
* App identity block for branding + version. Consumed by the `AdminPublicBootController`
|
|
704
|
+
* and the `AdminManifestConfigFactory`; the SuperAdmin UI (platform
|
|
705
|
+
* LoginPage, AdminLayout brand block) reads the same fields via PublicBoot.
|
|
706
|
+
*
|
|
707
|
+
* `name` = brand display name (e.g. "DemoApp", "ClubApp").
|
|
708
|
+
* `label` = tag/subtitle in the brand block (e.g. "SuperAdmin").
|
|
709
|
+
* `version` = app version string (build info).
|
|
710
|
+
* `icon` = 2-character abbreviation for the logo badge (e.g. "ma", "da").
|
|
711
|
+
* `logoUrl` = optional URL to a PNG/SVG; if set, the UI renders an <img>
|
|
712
|
+
* instead of the initials badge.
|
|
713
|
+
*/
|
|
714
|
+
interface PlanCatalogApp {
|
|
715
|
+
name: string;
|
|
716
|
+
label?: string;
|
|
717
|
+
version?: string;
|
|
718
|
+
icon?: string;
|
|
719
|
+
logoUrl?: string;
|
|
720
|
+
}
|
|
721
|
+
/**
|
|
722
|
+
* Notice periods, one per rhythm.
|
|
723
|
+
*
|
|
724
|
+
* One number for both was the shape until 2026-08-27, and it could not be right
|
|
725
|
+
* for both: a yearly contract with a fortnight of notice is unusual, and a
|
|
726
|
+
* monthly contract with three months of notice is void against a consumer. The
|
|
727
|
+
* two are configured apart because real contracts set them apart.
|
|
728
|
+
*
|
|
729
|
+
* Both members are required. A missing rhythm would read as zero, and a silent
|
|
730
|
+
* zero is a commercial decision nobody made — the same defect one level below
|
|
731
|
+
* the one that moved these settings into the file.
|
|
732
|
+
*
|
|
733
|
+
* **No ceiling is enforced.** §309 Nr. 9 BGB limits the notice period in German
|
|
734
|
+
* consumer contracts to one month, and an installation serving businesses is
|
|
735
|
+
* not bound by it. The platform cannot know which it is, so the number is the
|
|
736
|
+
* consumer app's to choose and this is the sentence that says what it costs.
|
|
737
|
+
*/
|
|
738
|
+
interface CancellationNoticePeriods {
|
|
739
|
+
/** Days of notice for a monthly subscription. */
|
|
740
|
+
monthly: number;
|
|
741
|
+
/** Days of notice for a yearly subscription. */
|
|
742
|
+
yearly: number;
|
|
743
|
+
}
|
|
744
|
+
/**
|
|
745
|
+
* Plans a tenant may not reach or leave without talking to sales.
|
|
746
|
+
*
|
|
747
|
+
* `asTarget`: may not be selected via self-service — typically ENTERPRISE,
|
|
748
|
+
* which only a special contract activates. `asSource`: may not be left via
|
|
749
|
+
* self-service — typically an active special contract.
|
|
750
|
+
*
|
|
751
|
+
* Both lists are required and may be empty. An empty list says out loud that
|
|
752
|
+
* self-service reaches every plan, which is a decision rather than an omission.
|
|
753
|
+
*/
|
|
754
|
+
interface SelfServiceBlockedPlans {
|
|
755
|
+
asTarget: string[];
|
|
756
|
+
asSource: string[];
|
|
757
|
+
}
|
|
758
|
+
/**
|
|
759
|
+
* Commercial settings for the tenant-facing self-service routes.
|
|
760
|
+
*
|
|
761
|
+
* They live in `config/saas.yaml` and nowhere else: an operator reading the
|
|
762
|
+
* file has to be reading the values that are running, with no "unless somebody
|
|
763
|
+
* passed it in code" attached. The file is read at boot, so an edit lands on
|
|
764
|
+
* the next restart.
|
|
765
|
+
*/
|
|
766
|
+
interface PlanCatalogTenantBilling {
|
|
767
|
+
cancellationNoticeDays: CancellationNoticePeriods;
|
|
768
|
+
selfServiceBlockedPlans: SelfServiceBlockedPlans;
|
|
769
|
+
}
|
|
770
|
+
/**
|
|
771
|
+
* Who is told when the settings in the file change between two starts.
|
|
772
|
+
*
|
|
773
|
+
* The record inside the application is written whether or not anybody is
|
|
774
|
+
* named here; mail is the addition, never the substitute. Mailed only where an
|
|
775
|
+
* email port is bound — without one the boot log says so once, and the change
|
|
776
|
+
* is recorded in the application only.
|
|
777
|
+
*/
|
|
778
|
+
interface PlanCatalogNotifications {
|
|
779
|
+
/** Addresses mailed when a start finds the applied settings changed. */
|
|
780
|
+
settingsChanged?: string[];
|
|
781
|
+
}
|
|
782
|
+
/**
|
|
783
|
+
* The legal entity on the operator's side of every contract the installation
|
|
784
|
+
* concludes. A contract copies it on the day it is concluded; only the legal
|
|
785
|
+
* name is required while nothing is invoiced.
|
|
786
|
+
*/
|
|
787
|
+
interface PlanCatalogIssuer {
|
|
788
|
+
legalName: string;
|
|
789
|
+
addressLine1?: string;
|
|
790
|
+
addressLine2?: string;
|
|
791
|
+
postalCode?: string;
|
|
792
|
+
city?: string;
|
|
793
|
+
/** ISO 3166-1 alpha-2. */
|
|
794
|
+
country?: string;
|
|
795
|
+
vatId?: string;
|
|
796
|
+
taxNumber?: string;
|
|
797
|
+
/** Declares a changed identity as a correction of the same legal entity. */
|
|
798
|
+
correctionOf?: PlanCatalogIssuerCorrection;
|
|
799
|
+
}
|
|
800
|
+
/**
|
|
801
|
+
* What a changed issuer identity replaces, and why.
|
|
802
|
+
*
|
|
803
|
+
* The identity is the counterparty a contract names, so it moves under a
|
|
804
|
+
* running contract only as a correction of that same entity. Each field names
|
|
805
|
+
* the value the installation recorded before the change, `null` where it
|
|
806
|
+
* recorded none; a field the change leaves alone need not be named.
|
|
807
|
+
*/
|
|
808
|
+
interface PlanCatalogIssuerCorrection {
|
|
809
|
+
legalName?: string | null;
|
|
810
|
+
vatId?: string | null;
|
|
811
|
+
taxNumber?: string | null;
|
|
812
|
+
/** Why the same entity now reads differently. Kept in the settings record. */
|
|
813
|
+
reason: string;
|
|
814
|
+
}
|
|
815
|
+
/** How the parties contracts are concluded with are numbered. */
|
|
816
|
+
interface PlanCatalogSubscribers {
|
|
817
|
+
/**
|
|
818
|
+
* Put in front of every customer number assigned from the next start on.
|
|
819
|
+
* A number keeps the prefix it was assigned with.
|
|
820
|
+
*/
|
|
821
|
+
customerNumberPrefix?: string;
|
|
822
|
+
}
|
|
823
|
+
/** One account at a payment gateway, by the name `PlanCatalogPayments.accounts` gives it. */
|
|
824
|
+
interface PlanCatalogPaymentAccount {
|
|
825
|
+
/** The provider its bound adapter names itself as, e.g. `stripe`. */
|
|
826
|
+
provider: string;
|
|
827
|
+
/** What a new payment method may be at this account. Read for `newPaymentMethods` only. */
|
|
828
|
+
methods?: PaymentMethodType[];
|
|
829
|
+
}
|
|
830
|
+
/**
|
|
831
|
+
* The gateway accounts payment methods are taken through. Their keys are bound
|
|
832
|
+
* in code from the environment, never written into the file.
|
|
833
|
+
*/
|
|
834
|
+
interface PlanCatalogPayments {
|
|
835
|
+
/** The account a new payment method is taken at. Omitted, none is taken. */
|
|
836
|
+
newPaymentMethods?: string;
|
|
837
|
+
/** The origins a gateway's form may send a person back to, such as `https://app.example.com`. */
|
|
838
|
+
returnUrlOrigins: string[];
|
|
839
|
+
/** Every account taking new payment methods or holding a reference in use. */
|
|
840
|
+
accounts: Record<string, PlanCatalogPaymentAccount>;
|
|
841
|
+
}
|
|
842
|
+
/**
|
|
843
|
+
* The part of `config/saas.yaml` that is configuration rather than catalogue.
|
|
844
|
+
*
|
|
845
|
+
* Declared apart so it can be handed on whole — a database catalogue takes its
|
|
846
|
+
* settings from the file and its plans from the database — without anybody
|
|
847
|
+
* listing the blocks again. `CATALOGUE_KEYS` names what is left over.
|
|
848
|
+
*/
|
|
849
|
+
interface PlanCatalogSettings {
|
|
850
|
+
/** App identity (branding + version), see PlanCatalogApp. */
|
|
851
|
+
app: PlanCatalogApp;
|
|
852
|
+
/** ISO-4217 currency code. */
|
|
853
|
+
currency: string;
|
|
854
|
+
/** VAT rate in percent. */
|
|
855
|
+
vatRate: number;
|
|
856
|
+
/** Commercial settings for the tenant self-service routes. */
|
|
857
|
+
tenantBilling: PlanCatalogTenantBilling;
|
|
858
|
+
/** App-wide marketing configuration. Optional. */
|
|
859
|
+
marketing?: PlanCatalogMarketing;
|
|
860
|
+
/** Who is told when the settings change between two starts. Optional. */
|
|
861
|
+
notifications?: PlanCatalogNotifications;
|
|
862
|
+
/** The operator's side of every contract. Optional until invoicing requires it. */
|
|
863
|
+
issuer?: PlanCatalogIssuer;
|
|
864
|
+
/** How subscribers are numbered. Optional. */
|
|
865
|
+
subscribers?: PlanCatalogSubscribers;
|
|
866
|
+
/** The payment gateway accounts. Optional until payment methods are taken. */
|
|
867
|
+
payments?: PlanCatalogPayments;
|
|
868
|
+
}
|
|
869
|
+
interface PlanCatalog extends PlanCatalogSettings {
|
|
870
|
+
schemaVersion: 1;
|
|
871
|
+
features?: FeatureDef[];
|
|
872
|
+
/**
|
|
873
|
+
* Optional. When omitted, plans come exclusively from the
|
|
874
|
+
* AdminUI / DB table (Plans/PlanVersions lifecycle).
|
|
875
|
+
*/
|
|
876
|
+
plans?: PlanDef[];
|
|
877
|
+
}
|
|
878
|
+
|
|
709
879
|
type PromoCodeValueType = 'PERCENT' | 'ABSOLUTE';
|
|
710
880
|
type PromoCodeDurationType = 'ONCE' | 'MONTHS' | 'BILLING_CYCLES';
|
|
711
881
|
type PromoCodeStatus = 'ACTIVE' | 'PAUSED' | 'EXHAUSTED' | 'EXPIRED';
|
|
@@ -754,6 +924,11 @@ interface PromoCode {
|
|
|
754
924
|
validUntil: string | null;
|
|
755
925
|
maxRedemptions: number | null;
|
|
756
926
|
redemptionsCount: number;
|
|
927
|
+
/**
|
|
928
|
+
* Slots held for checkouts that started and have not concluded. A code has
|
|
929
|
+
* a free slot while `redemptionsCount + heldCount` is below `maxRedemptions`.
|
|
930
|
+
*/
|
|
931
|
+
heldCount: number;
|
|
757
932
|
appliesToPlans: PlanId[];
|
|
758
933
|
appliesToBilling: BillingCycle | null;
|
|
759
934
|
firstTimeCustomersOnly: boolean;
|
|
@@ -902,7 +1077,7 @@ interface VersionedEntityBase {
|
|
|
902
1077
|
* own migration).
|
|
903
1078
|
* - `startedAt` is the contract start of this booking.
|
|
904
1079
|
* - `minimumTermEndsAt` = end of the minimum term; `null` = no minimum term
|
|
905
|
-
* (platform default =
|
|
1080
|
+
* (platform default = no commitment, set service-side).
|
|
906
1081
|
* - `canceledAt` / `canceledEffectiveAt`: cancellation anchor vs. effective
|
|
907
1082
|
* date. Before the minimum term ends, `canceledEffectiveAt =
|
|
908
1083
|
* minimumTermEndsAt`, otherwise the subscription's period end.
|
|
@@ -919,6 +1094,18 @@ interface SubscriptionBundleRecord {
|
|
|
919
1094
|
minimumTermEndsAt: Date | null;
|
|
920
1095
|
canceledAt: Date | null;
|
|
921
1096
|
canceledEffectiveAt: Date | null;
|
|
1097
|
+
/**
|
|
1098
|
+
* The rhythm this booking is billed in, and the window it is billed for.
|
|
1099
|
+
*
|
|
1100
|
+
* A bundle's periods end on the day its plan's do — the first one short,
|
|
1101
|
+
* from the booking to the next occurrence of that day, and every one after
|
|
1102
|
+
* it anchor to anchor. Null on a booking made before these fields existed,
|
|
1103
|
+
* or on one whose plan has no period; readers fall back to the plan's
|
|
1104
|
+
* cycle, which is what every booking used before.
|
|
1105
|
+
*/
|
|
1106
|
+
billingCycle: string | null;
|
|
1107
|
+
currentPeriodStart: Date | null;
|
|
1108
|
+
currentPeriodEnd: Date | null;
|
|
922
1109
|
createdAt: Date;
|
|
923
1110
|
updatedAt: Date;
|
|
924
1111
|
}
|
|
@@ -932,14 +1119,27 @@ interface SubscriptionBundleRecord {
|
|
|
932
1119
|
interface SubscriptionBundleView extends SubscriptionBundleRecord {
|
|
933
1120
|
bundleKey: string | null;
|
|
934
1121
|
label: string | null;
|
|
935
|
-
|
|
1122
|
+
/**
|
|
1123
|
+
* What this booking is billed at, in the rhythm it was booked in and with
|
|
1124
|
+
* the plan's pricing override applied.
|
|
1125
|
+
*
|
|
1126
|
+
* It was `monthlyNet` until 2026-08-27 and carried the bundle's base
|
|
1127
|
+
* monthly price whatever the booking was — so a yearly booking of a bundle
|
|
1128
|
+
* priced 10 monthly and 100 yearly reported 10. The name was half the
|
|
1129
|
+
* defect: a field called `monthlyNet` on a yearly booking cannot be right.
|
|
1130
|
+
*/
|
|
1131
|
+
priceNet: number | null;
|
|
936
1132
|
}
|
|
937
1133
|
interface CreateSubscriptionBundleData {
|
|
938
1134
|
subscriptionId: string;
|
|
939
1135
|
bundleVersionId: string;
|
|
940
1136
|
startedAt: Date;
|
|
941
|
-
/**
|
|
1137
|
+
/** Null unless a commitment was configured or asked for. */
|
|
942
1138
|
minimumTermEndsAt?: Date | null;
|
|
1139
|
+
/** The rhythm and window worked out above this port. */
|
|
1140
|
+
billingCycle?: string | null;
|
|
1141
|
+
currentPeriodStart?: Date | null;
|
|
1142
|
+
currentPeriodEnd?: Date | null;
|
|
943
1143
|
}
|
|
944
1144
|
interface CancelSubscriptionBundleData {
|
|
945
1145
|
canceledAt: Date;
|
|
@@ -988,7 +1188,6 @@ interface BundlePricingOverride {
|
|
|
988
1188
|
*/
|
|
989
1189
|
interface BundleRow {
|
|
990
1190
|
id: string;
|
|
991
|
-
projectKey: string;
|
|
992
1191
|
bundleKey: string;
|
|
993
1192
|
label: string;
|
|
994
1193
|
description: string | null;
|
|
@@ -1027,7 +1226,6 @@ interface BundleVersionRow extends VersionedEntityBase {
|
|
|
1027
1226
|
* first BundleVersion via `CreateBundleVersionDraftData`.
|
|
1028
1227
|
*/
|
|
1029
1228
|
interface CreateBundleData {
|
|
1030
|
-
projectKey: string;
|
|
1031
1229
|
bundleKey: string;
|
|
1032
1230
|
label: string;
|
|
1033
1231
|
description?: string | null;
|
|
@@ -1036,9 +1234,9 @@ interface CreateBundleData {
|
|
|
1036
1234
|
i18n?: CatalogEntryI18n;
|
|
1037
1235
|
}
|
|
1038
1236
|
/**
|
|
1039
|
-
* Fields that may be changed on the bundle master. `bundleKey`
|
|
1040
|
-
*
|
|
1041
|
-
*
|
|
1237
|
+
* Fields that may be changed on the bundle master. `bundleKey` is
|
|
1238
|
+
* intentionally not here — master identity is immutable; whoever wants to
|
|
1239
|
+
* change it creates a new bundle and retires the old one.
|
|
1042
1240
|
*/
|
|
1043
1241
|
interface UpdateBundleData {
|
|
1044
1242
|
label?: string;
|
|
@@ -1143,34 +1341,291 @@ interface PublishBundleVersionData {
|
|
|
1143
1341
|
validUntil?: string | null;
|
|
1144
1342
|
}
|
|
1145
1343
|
/**
|
|
1146
|
-
* Code of a strict-mode violation. lists the eight rules
|
|
1147
|
-
* that are checked; each rule has its own code so the UI can show
|
|
1148
|
-
* focused help texts.
|
|
1344
|
+
* Code of a strict-mode violation. lists the eight rules
|
|
1345
|
+
* that are checked; each rule has its own code so the UI can show
|
|
1346
|
+
* focused help texts.
|
|
1347
|
+
*/
|
|
1348
|
+
type StrictModeWarningCode = 'CAPABILITY_MISSING' | 'CAPABILITY_RETIRED' | 'FEATURE_MISSING' | 'FEATURE_PLANNED_ONLY' | 'BUNDLE_FEATURE_UNKNOWN' | 'BUNDLE_PLAN_KEY_UNKNOWN' | 'PLAN_FEATURE_UNKNOWN' | 'PLAN_FEATURE_NOT_APPROVED' | 'BUNDLE_FEATURE_NOT_APPROVED' | 'PLAN_FEATURE_DEPENDENCY_UNSATISFIED' | 'BUNDLE_FEATURE_DEPENDENCY_UNSATISFIED' | 'QUOTA_MISSING' | 'QUOTA_NOT_APPROVED' | 'VERSION_PUBLISH_OVERLAP';
|
|
1349
|
+
/**
|
|
1350
|
+
* A strict-mode violation. `field` points to the violating field
|
|
1351
|
+
* (e.g. `'features[3]'`), `value` is the concrete value (e.g. `'INVENTORY'`).
|
|
1352
|
+
*/
|
|
1353
|
+
interface StrictModeWarning {
|
|
1354
|
+
code: StrictModeWarningCode;
|
|
1355
|
+
/** Human-readable reason (German). */
|
|
1356
|
+
message: string;
|
|
1357
|
+
/** Path to the violating field; optional. */
|
|
1358
|
+
field?: string;
|
|
1359
|
+
/** The concrete violating value; optional. */
|
|
1360
|
+
value?: string;
|
|
1361
|
+
}
|
|
1362
|
+
/**
|
|
1363
|
+
* Service result for mutating Bundle operations
|
|
1364
|
+
* (createDraft, updateDraft, publish): returns the persisted row plus
|
|
1365
|
+
* a list of strict-mode warnings. In `warn-only` mode the
|
|
1366
|
+
* warnings go into the UI as a banner; in `blocking` mode the service throws
|
|
1367
|
+
* HTTP 422 instead, with the same warning list as the body.
|
|
1368
|
+
*/
|
|
1369
|
+
interface BundleVersionMutationResult {
|
|
1370
|
+
bundleVersion: BundleVersionRow;
|
|
1371
|
+
warnings: StrictModeWarning[];
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
/**
|
|
1375
|
+
* The column values a new BundleVersion draft starts from.
|
|
1376
|
+
*
|
|
1377
|
+
* Every adapter has to apply the same defaults — an absent quota map is `{}`,
|
|
1378
|
+
* an absent price is null rather than zero, an unstated `marketed` is true —
|
|
1379
|
+
* and two adapters spelling that out separately is the same decision written
|
|
1380
|
+
* twice. It is also the variant jscpd does catch, which is how this came out:
|
|
1381
|
+
* `adapter-drizzle` learning about bundles put a second copy beside
|
|
1382
|
+
* `adapter-prisma`'s.
|
|
1383
|
+
*
|
|
1384
|
+
* Validity windows are deliberately absent. Whether a draft carries
|
|
1385
|
+
* `validFrom`/`validUntil` is an adapter capability rather than a default, and
|
|
1386
|
+
* an adapter that does not maintain those columns must not write them.
|
|
1387
|
+
*/
|
|
1388
|
+
declare function bundleDraftDefaults(data: CreateBundleVersionDraftData): {
|
|
1389
|
+
baseVersionId: string | null;
|
|
1390
|
+
features: string[];
|
|
1391
|
+
quotas: Record<string, number>;
|
|
1392
|
+
compatibility: Record<string, unknown>;
|
|
1393
|
+
pricingOverrides: unknown[];
|
|
1394
|
+
monthlyNet: string | null;
|
|
1395
|
+
yearlyNet: string | null;
|
|
1396
|
+
marketed: boolean;
|
|
1397
|
+
changeNote: string;
|
|
1398
|
+
createdByUserId: string | null;
|
|
1399
|
+
};
|
|
1400
|
+
/**
|
|
1401
|
+
* The column values a new Bundle stem starts from.
|
|
1402
|
+
*
|
|
1403
|
+
* The same defaulting rule as above, one level up: an absent description or
|
|
1404
|
+
* icon is null rather than an empty string, an unstated sort order is 0, an
|
|
1405
|
+
* absent translation map is `{}`. Written out in five places before this — two
|
|
1406
|
+
* adapters and two fakes — which is four opportunities for one of them to
|
|
1407
|
+
* decide differently.
|
|
1408
|
+
*/
|
|
1409
|
+
declare function bundleStemDefaults(data: CreateBundleData): {
|
|
1410
|
+
bundleKey: string;
|
|
1411
|
+
label: string;
|
|
1412
|
+
description: string | null;
|
|
1413
|
+
icon: string | null;
|
|
1414
|
+
sortOrder: number;
|
|
1415
|
+
i18n: CatalogEntryI18n;
|
|
1416
|
+
};
|
|
1417
|
+
/** The stored shape both adapters read a bundle stem back from. */
|
|
1418
|
+
interface StoredBundleStem {
|
|
1419
|
+
id: string;
|
|
1420
|
+
bundleKey: string;
|
|
1421
|
+
label: string;
|
|
1422
|
+
description: string | null;
|
|
1423
|
+
icon: string | null;
|
|
1424
|
+
sortOrder: number;
|
|
1425
|
+
i18n: unknown;
|
|
1426
|
+
createdAt: Date;
|
|
1427
|
+
updatedAt: Date;
|
|
1428
|
+
deletedAt: Date | null;
|
|
1429
|
+
}
|
|
1430
|
+
/**
|
|
1431
|
+
* A stored bundle stem as the port describes it.
|
|
1432
|
+
*
|
|
1433
|
+
* The two stores spell the columns identically, so the mapping was identical
|
|
1434
|
+
* too — and an identical mapping in two files is one place for a field to be
|
|
1435
|
+
* forgotten when the row grows. `i18n` arrives as JSON of unknown shape from
|
|
1436
|
+
* both, and a non-object becomes `{}` rather than reaching a caller that
|
|
1437
|
+
* expects a map.
|
|
1149
1438
|
*/
|
|
1150
|
-
|
|
1439
|
+
declare function toBundleStemRow(row: StoredBundleStem): BundleRow;
|
|
1151
1440
|
/**
|
|
1152
|
-
*
|
|
1153
|
-
*
|
|
1441
|
+
* The fields a caller actually gave, as a patch.
|
|
1442
|
+
*
|
|
1443
|
+
* The update DTOs in this codebase mean three different things by three
|
|
1444
|
+
* different values: a value changes the column, an explicit `null` clears it,
|
|
1445
|
+
* and an **omitted** field leaves it alone. Only the last one needs care, and
|
|
1446
|
+
* it was written out as `...(data.x !== undefined ? { x: data.x } : {})` more
|
|
1447
|
+
* than fifty times across five repositories — one decision, fifty
|
|
1448
|
+
* opportunities to spell it differently, and the duplication ratchet is what
|
|
1449
|
+
* finally pointed at it.
|
|
1450
|
+
*
|
|
1451
|
+
* `null` is deliberately kept: it is a value a caller chose, not an absence.
|
|
1154
1452
|
*/
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1453
|
+
declare function definedFields<T extends object, K extends keyof T>(data: T, keys: readonly K[]): Partial<Pick<T, K>>;
|
|
1454
|
+
|
|
1455
|
+
/** Backend capability key, convention: domain.action[.action]. */
|
|
1456
|
+
type CapabilityKey = string;
|
|
1457
|
+
/** Frontend action-registry key. Same convention as CapabilityKey. */
|
|
1458
|
+
type ActionKey = CapabilityKey;
|
|
1459
|
+
/** Lookup key in the static extensions: map of the UI build. */
|
|
1460
|
+
type ComponentKey = string;
|
|
1461
|
+
interface AdminManifest {
|
|
1462
|
+
schemaVersion: 1;
|
|
1463
|
+
project: {
|
|
1464
|
+
key: string;
|
|
1465
|
+
displayName: string;
|
|
1466
|
+
/** Tag/subtitle (e.g. "SuperAdmin"). From `saas.yaml#app.label`. */
|
|
1467
|
+
label?: string;
|
|
1468
|
+
/** Short abbreviation for the logo badge (e.g. "ma", "da"). From `saas.yaml#app.icon`. */
|
|
1469
|
+
icon?: string;
|
|
1470
|
+
logoUrl?: string;
|
|
1471
|
+
environment?: 'production' | 'staging' | 'development';
|
|
1472
|
+
/**
|
|
1473
|
+
* Allowed locale pool from the app config (`saas.yaml`
|
|
1474
|
+
* `marketing.availableLocales`). First = default..
|
|
1475
|
+
*/
|
|
1476
|
+
availableLocales?: string[];
|
|
1477
|
+
/** Default locale; equals `availableLocales[0]`. */
|
|
1478
|
+
defaultLocale?: string;
|
|
1479
|
+
};
|
|
1480
|
+
build: {
|
|
1481
|
+
platformPackageVersion: string;
|
|
1482
|
+
appVersion: string;
|
|
1483
|
+
manifestHash: string;
|
|
1484
|
+
};
|
|
1485
|
+
planCatalogSnapshot: {
|
|
1486
|
+
source: string;
|
|
1487
|
+
hash: string;
|
|
1488
|
+
currency: string;
|
|
1489
|
+
vatRate: number;
|
|
1490
|
+
features?: FeatureDef[];
|
|
1491
|
+
plans: PlanDef[];
|
|
1492
|
+
};
|
|
1493
|
+
/** Map CapabilityKey → boolean. Manifest is never a security source. */
|
|
1494
|
+
capabilities: Record<CapabilityKey, boolean>;
|
|
1495
|
+
navigation: {
|
|
1496
|
+
standardPages: Partial<Record<StandardPageKey, StandardPageDef>>;
|
|
1497
|
+
projectPages?: ProjectPageDef[];
|
|
1498
|
+
};
|
|
1499
|
+
dashboard?: {
|
|
1500
|
+
kpiCards?: KpiCardDef[];
|
|
1501
|
+
};
|
|
1502
|
+
tenants?: {
|
|
1503
|
+
columns?: TenantColumnDef[];
|
|
1504
|
+
actions?: TenantActionDef[];
|
|
1505
|
+
};
|
|
1506
|
+
audit?: {
|
|
1507
|
+
actions?: AuditActionDef[];
|
|
1508
|
+
};
|
|
1163
1509
|
}
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
*/
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1510
|
+
type StandardPageKey = 'dashboard' | 'tenants' | 'subscriptions' | 'promoCodes' | 'plans' | 'audit' | 'users' | 'pilots' | 'discovery' | 'bundles' | 'marketingCatalog' | 'platformEmail' | 'platformEmailHistory' | 'settings';
|
|
1511
|
+
interface StandardPageDef {
|
|
1512
|
+
enabled: boolean;
|
|
1513
|
+
requiredCapability?: CapabilityKey;
|
|
1514
|
+
}
|
|
1515
|
+
interface ProjectPageDef {
|
|
1516
|
+
/** `<app>.<area>`, e.g. `demoapp.datev`. */
|
|
1517
|
+
id: string;
|
|
1518
|
+
label: string;
|
|
1519
|
+
icon?: string;
|
|
1520
|
+
/** Frontend route, e.g. `/admin/datev`. */
|
|
1521
|
+
route: string;
|
|
1522
|
+
navSection?: string;
|
|
1523
|
+
/** Lookup in the static extensions: map of the shell build. */
|
|
1524
|
+
componentKey: ComponentKey;
|
|
1525
|
+
requiredCapability?: CapabilityKey;
|
|
1526
|
+
prefetchOnIdle?: boolean;
|
|
1527
|
+
}
|
|
1528
|
+
interface KpiCardDef {
|
|
1529
|
+
id: string;
|
|
1530
|
+
label: string;
|
|
1531
|
+
/** Required path: /api/v1/admin/(extras|dashboard)/... */
|
|
1532
|
+
endpoint: string;
|
|
1533
|
+
displayHint: KpiDisplayHint;
|
|
1534
|
+
/** 0–100; UI sorts descending. */
|
|
1535
|
+
slotPriority?: number;
|
|
1536
|
+
requiredCapability?: CapabilityKey;
|
|
1537
|
+
}
|
|
1538
|
+
interface KpiDisplayHint {
|
|
1539
|
+
type: 'value' | 'value+timestamp' | 'value+spark8w' | 'value+delta';
|
|
1540
|
+
icon?: string;
|
|
1541
|
+
}
|
|
1542
|
+
interface TenantColumnDef {
|
|
1543
|
+
key: string;
|
|
1544
|
+
label: string;
|
|
1545
|
+
/** Required path: /api/v1/admin/extras/...; MUST be batch-capable, no {slug}/{tenantId}. */
|
|
1546
|
+
endpoint: string;
|
|
1547
|
+
requiredCapability?: CapabilityKey;
|
|
1548
|
+
}
|
|
1549
|
+
interface TenantActionDef {
|
|
1550
|
+
/** `<app>.<area>.<verb>`, e.g. `demoapp.datev.runExport`. */
|
|
1551
|
+
id: string;
|
|
1552
|
+
label: string;
|
|
1553
|
+
/** Lookup in the static actions: map of the shell build. */
|
|
1554
|
+
actionKey: ActionKey;
|
|
1555
|
+
requiredCapability?: CapabilityKey;
|
|
1556
|
+
requiresMfa?: boolean;
|
|
1557
|
+
confirmType?: 'none' | 'simple' | 'typed-slug' | 'typed-production' | 'date';
|
|
1558
|
+
}
|
|
1559
|
+
interface AuditActionDef {
|
|
1560
|
+
/** SCREAMING_SNAKE_CASE; matched to the AuditLog.action column. */
|
|
1561
|
+
key: string;
|
|
1562
|
+
label: string;
|
|
1563
|
+
severity?: 'info' | 'low' | 'medium' | 'high';
|
|
1564
|
+
}
|
|
1565
|
+
interface ManifestContribution {
|
|
1566
|
+
capabilities?: Record<CapabilityKey, boolean>;
|
|
1567
|
+
navigation?: {
|
|
1568
|
+
standardPages?: Partial<Record<StandardPageKey, StandardPageDef>>;
|
|
1569
|
+
projectPages?: ProjectPageDef[];
|
|
1570
|
+
};
|
|
1571
|
+
dashboard?: {
|
|
1572
|
+
kpiCards?: KpiCardDef[];
|
|
1573
|
+
};
|
|
1574
|
+
tenants?: {
|
|
1575
|
+
columns?: TenantColumnDef[];
|
|
1576
|
+
actions?: TenantActionDef[];
|
|
1577
|
+
};
|
|
1578
|
+
audit?: {
|
|
1579
|
+
actions?: AuditActionDef[];
|
|
1580
|
+
};
|
|
1581
|
+
}
|
|
1582
|
+
interface PublicBootResponse {
|
|
1583
|
+
project: {
|
|
1584
|
+
key: string;
|
|
1585
|
+
displayName: string;
|
|
1586
|
+
/** Tag/subtitle (e.g. "SuperAdmin"). From `saas.yaml#app.label`. */
|
|
1587
|
+
label?: string;
|
|
1588
|
+
/** Short abbreviation for the logo badge (e.g. "ma", "da"). From `saas.yaml#app.icon`. */
|
|
1589
|
+
icon?: string;
|
|
1590
|
+
logoUrl?: string;
|
|
1591
|
+
environment?: 'production' | 'staging' | 'development';
|
|
1592
|
+
};
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
/** Format: 'web:<email>:<sessionId>' or 'cli:<email>:<host>'. */
|
|
1596
|
+
type ActorTag = string;
|
|
1597
|
+
interface AuditEntry {
|
|
1598
|
+
id: string;
|
|
1599
|
+
/** null = platform action without tenant context (SUPER_ADMIN). */
|
|
1600
|
+
tenantId: string | null;
|
|
1601
|
+
/** null = system / cron-triggered. */
|
|
1602
|
+
userId: string | null;
|
|
1603
|
+
/** Convenience field; backend resolves it from userId. */
|
|
1604
|
+
userEmail: string | null;
|
|
1605
|
+
/** e.g. 'Tenant', 'PromoCode', 'Subscription', 'PlanVersion', 'User'. */
|
|
1606
|
+
entity: string;
|
|
1607
|
+
entityId: string;
|
|
1608
|
+
/** SCREAMING_SNAKE_CASE; past-tense oriented. */
|
|
1609
|
+
action: string;
|
|
1610
|
+
/** Freely structured. Convention: { field: { old, new } } or { reason, ... }. */
|
|
1611
|
+
changes: Record<string, unknown> | null;
|
|
1612
|
+
actorTag: ActorTag | null;
|
|
1613
|
+
ipAddress: string | null;
|
|
1614
|
+
userAgent: string | null;
|
|
1615
|
+
createdAt: string;
|
|
1616
|
+
}
|
|
1617
|
+
interface AuditQuery {
|
|
1618
|
+
tenantId?: string;
|
|
1619
|
+
userId?: string;
|
|
1620
|
+
entity?: string;
|
|
1621
|
+
entityId?: string;
|
|
1622
|
+
action?: string;
|
|
1623
|
+
/** Wildcard-capable, e.g. 'cli:*'. */
|
|
1624
|
+
actorTag?: string;
|
|
1625
|
+
from?: string;
|
|
1626
|
+
to?: string;
|
|
1627
|
+
page?: number;
|
|
1628
|
+
pageSize?: number;
|
|
1174
1629
|
}
|
|
1175
1630
|
|
|
1176
1631
|
/** Promotion type. */
|
|
@@ -1201,7 +1656,6 @@ type PromotionI18n = Record<string, PromotionI18nFields>;
|
|
|
1201
1656
|
/** Wire format of a `promotions` row. */
|
|
1202
1657
|
interface PromotionRow {
|
|
1203
1658
|
id: string;
|
|
1204
|
-
projectKey: string;
|
|
1205
1659
|
/** Internal label (not public). */
|
|
1206
1660
|
internalLabel: string;
|
|
1207
1661
|
type: PromotionType;
|
|
@@ -1227,11 +1681,7 @@ interface PromotionRow {
|
|
|
1227
1681
|
createdAt: string;
|
|
1228
1682
|
updatedAt: string;
|
|
1229
1683
|
}
|
|
1230
|
-
interface PromotionFilter {
|
|
1231
|
-
projectKey: string;
|
|
1232
|
-
}
|
|
1233
1684
|
interface CreatePromotionData {
|
|
1234
|
-
projectKey: string;
|
|
1235
1685
|
internalLabel: string;
|
|
1236
1686
|
type: PromotionType;
|
|
1237
1687
|
value: PromotionValue;
|
|
@@ -1293,8 +1743,35 @@ type PromotionResult = {
|
|
|
1293
1743
|
original: number;
|
|
1294
1744
|
months: number;
|
|
1295
1745
|
};
|
|
1296
|
-
/**
|
|
1746
|
+
/**
|
|
1747
|
+
* Applies the promotion math to a base price, or `null` where the promotion
|
|
1748
|
+
* takes nothing off it.
|
|
1749
|
+
*
|
|
1750
|
+
* The discounted price stays between 0 and the base price, whatever the
|
|
1751
|
+
* promotion states: a promotion lowers the price of what it is on and nothing
|
|
1752
|
+
* else. Creating one refuses a value no price could make sense of, but whether
|
|
1753
|
+
* an intro price or an amount fits depends on the price it meets — which
|
|
1754
|
+
* differs per plan and rhythm and moves when a new version is published — so
|
|
1755
|
+
* the bound is held here, where every place that resolves a promotion reads it:
|
|
1756
|
+
* the public catalogue, a checkout offer, the operator's preview.
|
|
1757
|
+
*/
|
|
1297
1758
|
declare function applyPromo(promo: PromotionRow | null, basePrice: number | null): PromotionResult | null;
|
|
1759
|
+
/** A promotion on a price, and what it makes of that price. */
|
|
1760
|
+
interface PromotionOnPrice {
|
|
1761
|
+
promotion: PromotionRow;
|
|
1762
|
+
result: PromotionResult;
|
|
1763
|
+
}
|
|
1764
|
+
/**
|
|
1765
|
+
* The promotion a price carries: the one `pickActivePromo` selects for the key,
|
|
1766
|
+
* language and rhythm, where `applyPromo` finds that it lowers the price at
|
|
1767
|
+
* all — or `null`.
|
|
1768
|
+
*
|
|
1769
|
+
* One question, asked the same way by every place that shows or charges a
|
|
1770
|
+
* promotion: the public catalogue, a checkout offer, and the operator's
|
|
1771
|
+
* preview. A promotion that lowers nothing is no promotion there, so it carries
|
|
1772
|
+
* no badge and takes nothing off.
|
|
1773
|
+
*/
|
|
1774
|
+
declare function promotionOnPrice(promotions: PromotionRow[], targetKey: string, locale: string, cycle: 'monthly' | 'yearly', basePrice: number | null, today?: Date, targetType?: PromotionTargetType): PromotionOnPrice | null;
|
|
1298
1775
|
|
|
1299
1776
|
type CheckoutOfferLineItemKind = 'plan' | 'bundle' | 'discount';
|
|
1300
1777
|
/** Frozen billable line item in the offer. */
|
|
@@ -1346,6 +1823,7 @@ interface CheckoutOfferPriceBreakdown {
|
|
|
1346
1823
|
regularNet: number;
|
|
1347
1824
|
/** Net total after promo. */
|
|
1348
1825
|
effectiveNet: number;
|
|
1826
|
+
/** VAT rate in percent, as the plan catalogue names it (19 = 19 %). */
|
|
1349
1827
|
vatRate: number;
|
|
1350
1828
|
/** Gross total after promo. */
|
|
1351
1829
|
effectiveGross: number;
|
|
@@ -1354,7 +1832,6 @@ type CheckoutOfferStatus = 'open' | 'consumed' | 'expired';
|
|
|
1354
1832
|
/** Wire format of a `checkout_offers` row. */
|
|
1355
1833
|
interface CheckoutOfferRow {
|
|
1356
1834
|
id: string;
|
|
1357
|
-
projectKey: string;
|
|
1358
1835
|
/** Plan selected on the website. */
|
|
1359
1836
|
planKey: string;
|
|
1360
1837
|
/** Resolved plan version, if known. */
|
|
@@ -1362,7 +1839,7 @@ interface CheckoutOfferRow {
|
|
|
1362
1839
|
billingCycle: 'monthly' | 'yearly';
|
|
1363
1840
|
/** Applied promotion (active at offer time). */
|
|
1364
1841
|
promotionId: string | null;
|
|
1365
|
-
/**
|
|
1842
|
+
/** Promo code applied to the offer, as the promo module normalised it. */
|
|
1366
1843
|
promoCode: string | null;
|
|
1367
1844
|
/** Added bundle keys. Legacy display; V3 uses `bundleVersionIds` + `lineItems`. */
|
|
1368
1845
|
bundles: string[];
|
|
@@ -1385,12 +1862,44 @@ interface CheckoutOfferRow {
|
|
|
1385
1862
|
updatedAt: string;
|
|
1386
1863
|
}
|
|
1387
1864
|
interface CheckoutOfferFilter {
|
|
1388
|
-
projectKey: string;
|
|
1389
1865
|
status?: CheckoutOfferStatus;
|
|
1390
1866
|
}
|
|
1391
|
-
/**
|
|
1867
|
+
/**
|
|
1868
|
+
* What a caller chooses: the body of `POST /public/checkout-offer`, and the
|
|
1869
|
+
* input of `CheckoutOfferService.create`.
|
|
1870
|
+
*
|
|
1871
|
+
* No amount is part of it. The plan version, the bundle prices, the promotion
|
|
1872
|
+
* and the promo code discount are resolved on the server, so the offer costs
|
|
1873
|
+
* what the catalogue says rather than what a request says.
|
|
1874
|
+
*/
|
|
1875
|
+
interface CheckoutOfferSelection {
|
|
1876
|
+
planKey: string;
|
|
1877
|
+
billingCycle: 'monthly' | 'yearly';
|
|
1878
|
+
/** Concrete BundleVersion IDs to book with the plan. */
|
|
1879
|
+
bundleVersionIds?: string[];
|
|
1880
|
+
/** A promo code to apply; refused when the promo module cannot accept it. */
|
|
1881
|
+
promoCode?: string | null;
|
|
1882
|
+
locale?: string;
|
|
1883
|
+
validUntil?: string | null;
|
|
1884
|
+
}
|
|
1885
|
+
/**
|
|
1886
|
+
* What a caller may change while an offer is open: the body of
|
|
1887
|
+
* `PATCH /public/checkout-offer/:id`. The plan is fixed; everything given here
|
|
1888
|
+
* is priced again.
|
|
1889
|
+
*/
|
|
1890
|
+
interface CheckoutOfferSelectionUpdate {
|
|
1891
|
+
billingCycle?: 'monthly' | 'yearly';
|
|
1892
|
+
bundleVersionIds?: string[];
|
|
1893
|
+
/** `null` removes a code applied before. */
|
|
1894
|
+
promoCode?: string | null;
|
|
1895
|
+
locale?: string;
|
|
1896
|
+
validUntil?: string | null;
|
|
1897
|
+
}
|
|
1898
|
+
/**
|
|
1899
|
+
* A new offer as the repository stores it — the selection with the amounts
|
|
1900
|
+
* the server computed for it (`CheckoutOfferRepository.create`).
|
|
1901
|
+
*/
|
|
1392
1902
|
interface CreateCheckoutOfferData {
|
|
1393
|
-
projectKey: string;
|
|
1394
1903
|
planKey: string;
|
|
1395
1904
|
planVersionId?: string | null;
|
|
1396
1905
|
billingCycle: 'monthly' | 'yearly';
|
|
@@ -1406,11 +1915,13 @@ interface CreateCheckoutOfferData {
|
|
|
1406
1915
|
validUntil?: string | null;
|
|
1407
1916
|
}
|
|
1408
1917
|
/**
|
|
1409
|
-
*
|
|
1410
|
-
*
|
|
1411
|
-
* sets them server-side.
|
|
1918
|
+
* A change to an open offer as the repository stores it, with the amounts
|
|
1919
|
+
* priced again (`CheckoutOfferRepository.update`). `status`/`consumedAt` are
|
|
1920
|
+
* not editable — `consume()` sets them server-side.
|
|
1412
1921
|
*/
|
|
1413
1922
|
interface UpdateCheckoutOfferData {
|
|
1923
|
+
/** The plan version active when the change was priced. */
|
|
1924
|
+
planVersionId?: string | null;
|
|
1414
1925
|
billingCycle?: 'monthly' | 'yearly';
|
|
1415
1926
|
promotionId?: string | null;
|
|
1416
1927
|
promoCode?: string | null;
|
|
@@ -1426,7 +1937,6 @@ interface UpdateCheckoutOfferData {
|
|
|
1426
1937
|
|
|
1427
1938
|
/** Wire format of the `marketing_settings` row. */
|
|
1428
1939
|
interface MarketingSettingsRow {
|
|
1429
|
-
projectKey: string;
|
|
1430
1940
|
/** Runtime-activated subset of the `availableLocales` pool. */
|
|
1431
1941
|
activeLocales: string[];
|
|
1432
1942
|
updatedAt: string;
|
|
@@ -1462,6 +1972,10 @@ interface PublicMarketingPlan {
|
|
|
1462
1972
|
badge: string;
|
|
1463
1973
|
/** Teaser / description text. */
|
|
1464
1974
|
description: string;
|
|
1975
|
+
/**
|
|
1976
|
+
* The recommended plan, and at most one card in a catalogue carries it —
|
|
1977
|
+
* see `keepOneRecommended`, which decides it per language served.
|
|
1978
|
+
*/
|
|
1465
1979
|
highlight: boolean;
|
|
1466
1980
|
/**
|
|
1467
1981
|
* Formatted pricing tag from the MarketingProjection (#47, e.g.
|
|
@@ -1542,13 +2056,32 @@ interface PublicComparisonRow {
|
|
|
1542
2056
|
/** Quotas only: display unit. */
|
|
1543
2057
|
unit?: string;
|
|
1544
2058
|
}
|
|
2059
|
+
/**
|
|
2060
|
+
* What this installation takes a new payment method with, so a page offering the
|
|
2061
|
+
* plans can say it before anybody reaches the form.
|
|
2062
|
+
*
|
|
2063
|
+
* It is the account `config/saas.yaml#payments.newPaymentMethods` names, with the
|
|
2064
|
+
* gateway bound for it — the same account a sign-up's payment step and a tenant's
|
|
2065
|
+
* own change both go to. Whether an installation runs sign-ups at all is not part
|
|
2066
|
+
* of it: that is the application's own wiring, and the application knows it.
|
|
2067
|
+
*
|
|
2068
|
+
* Which account it is and who keeps the payment method stay inside: a prospect is
|
|
2069
|
+
* told that a card or a direct debit will be asked for, not where it is kept.
|
|
2070
|
+
*/
|
|
2071
|
+
interface PublicNewPaymentMethods {
|
|
2072
|
+
/** Whether a new payment method is taken here at all. */
|
|
2073
|
+
taken: boolean;
|
|
2074
|
+
/** The methods the form offers, in the order the installation names them; empty where none is taken. */
|
|
2075
|
+
methods: PaymentMethodType[];
|
|
2076
|
+
}
|
|
1545
2077
|
/** Response of `GET /public/marketing-catalog`. */
|
|
1546
2078
|
interface PublicMarketingCatalogResponse {
|
|
1547
|
-
projectKey: string;
|
|
1548
2079
|
locale: string;
|
|
1549
2080
|
currency: string;
|
|
1550
2081
|
/** VAT rate in percent — for the CheckoutOffer price breakdown. */
|
|
1551
2082
|
vatRate: number;
|
|
2083
|
+
/** What a new payment method is taken with here, if one is taken. */
|
|
2084
|
+
newPaymentMethods: PublicNewPaymentMethods;
|
|
1552
2085
|
/** Visible, marketed plans — sorted by `priority` DESC. */
|
|
1553
2086
|
plans: PublicMarketingPlan[];
|
|
1554
2087
|
/**
|
|
@@ -1654,7 +2187,7 @@ interface DiscoverySnapshot {
|
|
|
1654
2187
|
/** ISO timestamp of the boot-time scan. */
|
|
1655
2188
|
scannedAt: string;
|
|
1656
2189
|
app: {
|
|
1657
|
-
/**
|
|
2190
|
+
/** The application's name, from `saas.yaml#app.name`. */
|
|
1658
2191
|
key: string;
|
|
1659
2192
|
/** Backend version, e.g. from package.json. */
|
|
1660
2193
|
version: string;
|
|
@@ -1690,6 +2223,18 @@ interface FeatureUiMeta {
|
|
|
1690
2223
|
/** Map FeatureKey → UI metadata. Consumer apps supply a complete table. */
|
|
1691
2224
|
type FeatureUiRegistry = Record<string, FeatureUiMeta>;
|
|
1692
2225
|
|
|
2226
|
+
/** A quota as a finite number, or `null` where the value cannot be read as one. */
|
|
2227
|
+
declare function readQuotaValue(value: unknown): number | null;
|
|
2228
|
+
/**
|
|
2229
|
+
* Every quota in a JSON column, for a caller that computes with them.
|
|
2230
|
+
*
|
|
2231
|
+
* A key that is there stays there. Dropping an unreadable one made it *absent*,
|
|
2232
|
+
* and absent means undeclared: `enforceLimit` answers an undeclared dimension
|
|
2233
|
+
* with a 500, so every operation on that quota was refused — a fail-closed
|
|
2234
|
+
* answer to somebody else's corrupt row.
|
|
2235
|
+
*/
|
|
2236
|
+
declare function readQuotaRecord(value: unknown): Record<string, number>;
|
|
2237
|
+
|
|
1693
2238
|
/** Alias for historical compatibility — equivalent to VersionChangeDirection. */
|
|
1694
2239
|
type ChangeDirection = VersionChangeDirection;
|
|
1695
2240
|
interface DiffResult {
|
|
@@ -1707,9 +2252,16 @@ type DecimalLike = number | string | {
|
|
|
1707
2252
|
};
|
|
1708
2253
|
interface PlanVersionFields {
|
|
1709
2254
|
features: FeatureKey[];
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
2255
|
+
/**
|
|
2256
|
+
* Quotas of the version. -1 = unlimited; missing key = 0.
|
|
2257
|
+
*
|
|
2258
|
+
* Every key either side carries is compared. Which keys exist is the
|
|
2259
|
+
* installation's decision — they come from `@DefinesQuota` — so a fixed
|
|
2260
|
+
* set here would have compared the three the platform happened to know by
|
|
2261
|
+
* name and let every other one be lowered without the confirmation
|
|
2262
|
+
* publishing a regression asks for.
|
|
2263
|
+
*/
|
|
2264
|
+
quotas: Record<QuotaKey, number>;
|
|
1713
2265
|
monthlyNet: DecimalLike;
|
|
1714
2266
|
yearlyNet: DecimalLike;
|
|
1715
2267
|
}
|
|
@@ -1725,8 +2277,7 @@ declare function classifyPlanDiff(oldV: PlanVersionFields, newV: PlanVersionFiel
|
|
|
1725
2277
|
/**
|
|
1726
2278
|
* Classification of a BundleVersion diff for contract protection.
|
|
1727
2279
|
*
|
|
1728
|
-
*
|
|
1729
|
-
* number. Otherwise higher = better. Missing keys are treated as 0.
|
|
2280
|
+
* Quotas are compared exactly as they are for a plan.
|
|
1730
2281
|
*
|
|
1731
2282
|
* Pricing can be `null` (the bundle only has override pricing); a switch
|
|
1732
2283
|
* from value ↔ null is classified as REGRESSION (value dropped) or IMPROVEMENT
|
|
@@ -1769,6 +2320,11 @@ interface PromoPreviewValidResponse {
|
|
|
1769
2320
|
/** Decimal-as-string, e.g. "199.00". */
|
|
1770
2321
|
originalGross: string;
|
|
1771
2322
|
discountGross: string;
|
|
2323
|
+
/**
|
|
2324
|
+
* `discountGross` in net, converted at the installation's VAT rate —
|
|
2325
|
+
* the figure a page showing net prices takes off the plan price.
|
|
2326
|
+
*/
|
|
2327
|
+
discountNet: string;
|
|
1772
2328
|
discountedGross: string;
|
|
1773
2329
|
includedVat: string;
|
|
1774
2330
|
nextRegularAmountGross: string;
|
|
@@ -1824,9 +2380,94 @@ interface OnboardingPromoRedemption {
|
|
|
1824
2380
|
endsAt: string | null;
|
|
1825
2381
|
}
|
|
1826
2382
|
|
|
2383
|
+
/**
|
|
2384
|
+
* The settings subtree of a plan catalogue: every top-level block that is
|
|
2385
|
+
* configuration rather than the catalogue itself. JSON-shaped, because it is
|
|
2386
|
+
* stored as JSON and compared as JSON.
|
|
2387
|
+
*/
|
|
2388
|
+
type AppliedSettingsValues = Record<string, unknown>;
|
|
2389
|
+
/** The one row per installation: what is applied, since when, and from where. */
|
|
2390
|
+
interface AppliedSettingsRecord {
|
|
2391
|
+
/**
|
|
2392
|
+
* `sha256-<hex>` over the canonical JSON of `settings`. Two boots with the
|
|
2393
|
+
* same resolved values produce the same fingerprint however the file was
|
|
2394
|
+
* formatted, and a plan added to the catalogue does not move it.
|
|
2395
|
+
*/
|
|
2396
|
+
fingerprint: string;
|
|
2397
|
+
settings: AppliedSettingsValues;
|
|
2398
|
+
/**
|
|
2399
|
+
* Where the values came from: the absolute path of the file the platform
|
|
2400
|
+
* read, or a phrase saying they were handed to it in code.
|
|
2401
|
+
*/
|
|
2402
|
+
source: string;
|
|
2403
|
+
/** The moment these values became the running configuration. */
|
|
2404
|
+
appliedAt: Date;
|
|
2405
|
+
}
|
|
2406
|
+
/** What a boot noticed had changed since the previous record. */
|
|
2407
|
+
interface SettingsChangeRecord {
|
|
2408
|
+
id: string;
|
|
2409
|
+
/** The boot that noticed the difference and applied the new values. */
|
|
2410
|
+
noticedAt: Date;
|
|
2411
|
+
source: string;
|
|
2412
|
+
previous: AppliedSettingsValues;
|
|
2413
|
+
current: AppliedSettingsValues;
|
|
2414
|
+
/** Set once an operator has seen it; null while it is still owed a look. */
|
|
2415
|
+
acknowledgedAt: Date | null;
|
|
2416
|
+
/** Who acknowledged it — an actor tag, as the audit log writes it. */
|
|
2417
|
+
acknowledgedBy: string | null;
|
|
2418
|
+
}
|
|
2419
|
+
type NewSettingsChange = Pick<SettingsChangeRecord, 'noticedAt' | 'source' | 'previous' | 'current'>;
|
|
2420
|
+
/** One leaf that differs between two settings subtrees. */
|
|
2421
|
+
interface SettingsDifference {
|
|
2422
|
+
/** Dotted path, as the loader names a field: `tenantBilling.cancellationNoticeDays.monthly`. */
|
|
2423
|
+
path: string;
|
|
2424
|
+
/** `undefined` where the leaf did not exist on that side. */
|
|
2425
|
+
before: unknown;
|
|
2426
|
+
after: unknown;
|
|
2427
|
+
}
|
|
2428
|
+
|
|
2429
|
+
/**
|
|
2430
|
+
* The top-level blocks of `config/saas.yaml` that are the catalogue rather than
|
|
2431
|
+
* the configuration, and the format marker.
|
|
2432
|
+
*
|
|
2433
|
+
* An exclusion list rather than a list of settings, on purpose: a block the
|
|
2434
|
+
* schema gains tomorrow is a setting until somebody says otherwise, so it is
|
|
2435
|
+
* fingerprinted by default. The failure mode of the other list — a new setting
|
|
2436
|
+
* silently left out of the fingerprint, so a change to it is never noticed — is
|
|
2437
|
+
* the one this record exists to prevent. `schemaVersion` is excluded because a
|
|
2438
|
+
* format change is a migration of the file, not a decision an operator took.
|
|
2439
|
+
*
|
|
2440
|
+
* `tests/settings-subtree.test.js` holds this list to the schema in both
|
|
2441
|
+
* directions: every name here is a property the schema declares, and every
|
|
2442
|
+
* property the schema declares lands on one side.
|
|
2443
|
+
*/
|
|
2444
|
+
declare const CATALOGUE_KEYS: ReadonlySet<keyof PlanCatalog>;
|
|
2445
|
+
/**
|
|
2446
|
+
* The settings of a catalogue, typed, for handing them on as a whole.
|
|
2447
|
+
*
|
|
2448
|
+
* The same selection as `settingsSubtreeOf`, which is why it is that function:
|
|
2449
|
+
* a caller that listed the blocks it passes on would drop the next one the
|
|
2450
|
+
* schema gains, and nothing would say so.
|
|
2451
|
+
*/
|
|
2452
|
+
declare function planCatalogSettingsOf(catalog: PlanCatalog): PlanCatalogSettings;
|
|
2453
|
+
/** Everything in the catalogue that is configuration, as it was resolved. */
|
|
2454
|
+
declare function settingsSubtreeOf(catalog: PlanCatalog | PlanCatalogSettings): AppliedSettingsValues;
|
|
2455
|
+
/**
|
|
2456
|
+
* `JSON.stringify` with object keys in sorted order at every depth, so that two
|
|
2457
|
+
* documents saying the same thing in a different order serialise identically.
|
|
2458
|
+
* Array order is kept: a list is what its author wrote, in the order they wrote
|
|
2459
|
+
* it.
|
|
2460
|
+
*/
|
|
2461
|
+
declare function canonicalJson(value: unknown): string;
|
|
2462
|
+
/**
|
|
2463
|
+
* Every leaf that differs between `before` and `after`, in the order the paths
|
|
2464
|
+
* sort. A list counts as one leaf: `asTarget: [] → [ENTERPRISE]` is one thing
|
|
2465
|
+
* that changed, not a change per element.
|
|
2466
|
+
*/
|
|
2467
|
+
declare function diffSettings(before: AppliedSettingsValues, after: AppliedSettingsValues): SettingsDifference[];
|
|
2468
|
+
|
|
1827
2469
|
interface PlanRow {
|
|
1828
2470
|
id: string;
|
|
1829
|
-
projectKey: string;
|
|
1830
2471
|
planKey: string;
|
|
1831
2472
|
label: string;
|
|
1832
2473
|
description: string | null;
|
|
@@ -1843,7 +2484,6 @@ interface PlanRow {
|
|
|
1843
2484
|
* creation (follows in M6 Pack 2).
|
|
1844
2485
|
*/
|
|
1845
2486
|
interface CreatePlanData {
|
|
1846
|
-
projectKey: string;
|
|
1847
2487
|
planKey: string;
|
|
1848
2488
|
label: string;
|
|
1849
2489
|
description?: string | null;
|
|
@@ -1851,9 +2491,9 @@ interface CreatePlanData {
|
|
|
1851
2491
|
sortOrder?: number;
|
|
1852
2492
|
}
|
|
1853
2493
|
/**
|
|
1854
|
-
* Fields that may be changed on the plan stem. `planKey`
|
|
1855
|
-
*
|
|
1856
|
-
*
|
|
2494
|
+
* Fields that may be changed on the plan stem. `planKey` is deliberately not
|
|
2495
|
+
* here — stem identity is immutable; whoever wants to change it creates a new
|
|
2496
|
+
* plan and retires the old one.
|
|
1857
2497
|
*/
|
|
1858
2498
|
interface UpdatePlanData {
|
|
1859
2499
|
label?: string;
|
|
@@ -1917,7 +2557,6 @@ interface UpsertResult {
|
|
|
1917
2557
|
skipReason?: string;
|
|
1918
2558
|
}
|
|
1919
2559
|
interface UpsertPlanInput {
|
|
1920
|
-
projectKey: string;
|
|
1921
2560
|
planKey: string;
|
|
1922
2561
|
label: string;
|
|
1923
2562
|
description?: string | null;
|
|
@@ -1937,7 +2576,6 @@ interface UpsertPlanVersionInput {
|
|
|
1937
2576
|
changeNote: string;
|
|
1938
2577
|
}
|
|
1939
2578
|
interface UpsertFeatureCatalogEntryInput {
|
|
1940
|
-
projectKey: string;
|
|
1941
2579
|
featureKey: FeatureKey;
|
|
1942
2580
|
label?: string;
|
|
1943
2581
|
icon?: string;
|
|
@@ -1983,7 +2621,7 @@ interface PlanCatalogReadSnapshot {
|
|
|
1983
2621
|
* implement it against their Prisma tables.
|
|
1984
2622
|
*/
|
|
1985
2623
|
interface PlanCatalogReadSink {
|
|
1986
|
-
loadSnapshot(
|
|
2624
|
+
loadSnapshot(): Promise<PlanCatalogReadSnapshot>;
|
|
1987
2625
|
}
|
|
1988
2626
|
|
|
1989
2627
|
/**
|
|
@@ -2220,6 +2858,26 @@ interface PasswordHasher {
|
|
|
2220
2858
|
hash(plain: string): Promise<string>;
|
|
2221
2859
|
verify(hash: string, plain: string): Promise<boolean>;
|
|
2222
2860
|
}
|
|
2861
|
+
/**
|
|
2862
|
+
* Sends a plain-text mail to an operator.
|
|
2863
|
+
*
|
|
2864
|
+
* The platform composes the text; the adapter delivers it — over whatever the
|
|
2865
|
+
* installation already sends mail with. Deliberately narrow: no templates, no
|
|
2866
|
+
* locale, no HTML. The one thing the platform mails today is a diagnostic for
|
|
2867
|
+
* the operator who runs the installation, and a diagnostic is English and
|
|
2868
|
+
* plain, like the boot log it mirrors. Tenant-facing mail — a verification
|
|
2869
|
+
* code, a resume link — goes through the registration module's own delivery
|
|
2870
|
+
* ports, which carry the locale and the person's name because that mail is
|
|
2871
|
+
* for a customer.
|
|
2872
|
+
*/
|
|
2873
|
+
interface EmailPort {
|
|
2874
|
+
/** Delivers one plain-text mail to one address; rejects when it cannot. */
|
|
2875
|
+
send(message: {
|
|
2876
|
+
to: string;
|
|
2877
|
+
subject: string;
|
|
2878
|
+
text: string;
|
|
2879
|
+
}): Promise<void>;
|
|
2880
|
+
}
|
|
2223
2881
|
/** Adapter for MFA secret persistence. */
|
|
2224
2882
|
interface MfaPort {
|
|
2225
2883
|
/** Returns the stored TOTP secret or null. */
|
|
@@ -2261,6 +2919,11 @@ interface PromoCodeRecord {
|
|
|
2261
2919
|
validUntil: Date | null;
|
|
2262
2920
|
maxRedemptions: number | null;
|
|
2263
2921
|
redemptionsCount: number;
|
|
2922
|
+
/**
|
|
2923
|
+
* Slots held for checkouts that started and have not concluded
|
|
2924
|
+
* (`PromoCodeHoldRepository`). An adapter without holds reports 0.
|
|
2925
|
+
*/
|
|
2926
|
+
heldCount: number;
|
|
2264
2927
|
appliesToPlans: string[];
|
|
2265
2928
|
appliesToBilling: BillingCycle | null;
|
|
2266
2929
|
firstTimeCustomersOnly: boolean;
|
|
@@ -2350,7 +3013,7 @@ interface PromoCodeRedemptionListItem extends PromoCodeRedemptionRecord {
|
|
|
2350
3013
|
/**
|
|
2351
3014
|
* Adapter for PromoCode persistence. Atomic slot reservation lives in the
|
|
2352
3015
|
* adapter because it is DB-specific (Postgres `UPDATE ... WHERE ... AND
|
|
2353
|
-
* (maxRedemptions IS NULL OR redemptionsCount < maxRedemptions)`).
|
|
3016
|
+
* (maxRedemptions IS NULL OR redemptionsCount + heldCount < maxRedemptions)`).
|
|
2354
3017
|
*/
|
|
2355
3018
|
interface PromoCodeRepository {
|
|
2356
3019
|
findById(id: string): Promise<PromoCodeRecord | null>;
|
|
@@ -2361,12 +3024,17 @@ interface PromoCodeRepository {
|
|
|
2361
3024
|
softDelete(id: string): Promise<void>;
|
|
2362
3025
|
/**
|
|
2363
3026
|
* Atomic slot reservation: increments `redemptionsCount` and checks
|
|
2364
|
-
* `status === 'ACTIVE' && (maxRedemptions IS NULL || redemptionsCount < maxRedemptions)`.
|
|
3027
|
+
* `status === 'ACTIVE' && (maxRedemptions IS NULL || redemptionsCount + heldCount < maxRedemptions)`.
|
|
2365
3028
|
* Returns true if the slot was reserved, false if EXHAUSTED
|
|
2366
|
-
* or the status is not ACTIVE.
|
|
3029
|
+
* or the status is not ACTIVE. A slot held for a checkout is not free, so
|
|
3030
|
+
* an adapter that keeps holds counts `heldCount` here.
|
|
2367
3031
|
*/
|
|
2368
3032
|
claimSlot(id: string, tx?: TransactionContext): Promise<boolean>;
|
|
2369
|
-
/**
|
|
3033
|
+
/**
|
|
3034
|
+
* Sets the status to `EXHAUSTED` when `redemptionsCount >= maxRedemptions`.
|
|
3035
|
+
* Holds do not count: a code full only because of held slots stays ACTIVE,
|
|
3036
|
+
* and gets its slots back when the holds end.
|
|
3037
|
+
*/
|
|
2370
3038
|
markExhaustedIfFull(id: string, tx?: TransactionContext): Promise<void>;
|
|
2371
3039
|
/** Decrements `redemptionsCount` by 1 (min 0); EXHAUSTED → ACTIVE. */
|
|
2372
3040
|
releaseSlot(id: string, tx?: TransactionContext): Promise<void>;
|
|
@@ -2376,6 +3044,98 @@ interface PromoCodeRepository {
|
|
|
2376
3044
|
*/
|
|
2377
3045
|
expireDueCodes(now: Date): Promise<number>;
|
|
2378
3046
|
}
|
|
3047
|
+
/**
|
|
3048
|
+
* A slot of a code kept for a checkout offer, from the start of its checkout
|
|
3049
|
+
* until the checkout concludes, the offer's code changes, or `expiresAt`
|
|
3050
|
+
* passes, whichever comes first. For a sign-up, `expiresAt` is the last moment
|
|
3051
|
+
* a confirmation of its payment form can arrive
|
|
3052
|
+
* (`PaymentMethodSetupSession.confirmableUntil`), not the checkout's lifetime.
|
|
3053
|
+
*/
|
|
3054
|
+
interface PromoCodeHoldRecord {
|
|
3055
|
+
id: string;
|
|
3056
|
+
promoCodeId: string;
|
|
3057
|
+
checkoutOfferId: string;
|
|
3058
|
+
expiresAt: Date;
|
|
3059
|
+
createdAt: Date;
|
|
3060
|
+
}
|
|
3061
|
+
/** What `PromoCodeHoldRepository.take` did. */
|
|
3062
|
+
type PromoCodeHoldTaken = {
|
|
3063
|
+
outcome: 'taken';
|
|
3064
|
+
hold: PromoCodeHoldRecord;
|
|
3065
|
+
}
|
|
3066
|
+
/** The code is not ACTIVE, is deleted, or has no free slot. */
|
|
3067
|
+
| {
|
|
3068
|
+
outcome: 'no-slot';
|
|
3069
|
+
}
|
|
3070
|
+
/** The offer holds a slot already, perhaps taken by a concurrent call. */
|
|
3071
|
+
| {
|
|
3072
|
+
outcome: 'offer-holds-one';
|
|
3073
|
+
};
|
|
3074
|
+
/**
|
|
3075
|
+
* Adapter for the slots a code keeps for checkouts. Every method that ends a
|
|
3076
|
+
* hold deletes its row and gives its slot back in one statement, so a hold is
|
|
3077
|
+
* counted in `PromoCodeRecord.heldCount` exactly as long as its row exists.
|
|
3078
|
+
*
|
|
3079
|
+
* Optional in the promo module: an adapter that has no holds leaves
|
|
3080
|
+
* `heldCount` at 0, and taking a hold then refuses to start rather than
|
|
3081
|
+
* quietly holding nothing.
|
|
3082
|
+
*/
|
|
3083
|
+
interface PromoCodeHoldRepository {
|
|
3084
|
+
/** The offer's hold, live or past its expiry and not yet given back. */
|
|
3085
|
+
findByCheckoutOffer(checkoutOfferId: string, tx?: TransactionContext): Promise<PromoCodeHoldRecord | null>;
|
|
3086
|
+
/**
|
|
3087
|
+
* Takes a slot of an ACTIVE, undeleted code for the offer, atomically with
|
|
3088
|
+
* `claimSlot`'s rule: `maxRedemptions IS NULL OR redemptionsCount +
|
|
3089
|
+
* heldCount < maxRedemptions`. An offer holds one slot at most. Runs on a
|
|
3090
|
+
* transaction of its own: a checkout starts outside any other, and the
|
|
3091
|
+
* slot is committed before the gateway's form opens.
|
|
3092
|
+
*/
|
|
3093
|
+
take(hold: {
|
|
3094
|
+
promoCodeId: string;
|
|
3095
|
+
checkoutOfferId: string;
|
|
3096
|
+
expiresAt: Date;
|
|
3097
|
+
}): Promise<PromoCodeHoldTaken>;
|
|
3098
|
+
/**
|
|
3099
|
+
* Moves the expiry of the offer's hold on that code to `expiresAt`, and
|
|
3100
|
+
* never earlier than it stands: the later of the two is written in the one
|
|
3101
|
+
* statement, so a start of the same checkout that asks for less cannot
|
|
3102
|
+
* shorten the slot a form opened by another start relies on, however the
|
|
3103
|
+
* two interleave. False when the offer holds no slot of it any more — it
|
|
3104
|
+
* ended in the meantime.
|
|
3105
|
+
*/
|
|
3106
|
+
extend(checkoutOfferId: string, promoCodeId: string, expiresAt: Date): Promise<boolean>;
|
|
3107
|
+
/** Ends the offer's hold and gives its slot back. False when it had none. */
|
|
3108
|
+
release(checkoutOfferId: string, tx?: TransactionContext): Promise<boolean>;
|
|
3109
|
+
/**
|
|
3110
|
+
* Ends the offer's hold and gives its slot back only while it still expires
|
|
3111
|
+
* at `expiresAt` — the hold as the caller wrote it. A hold another start
|
|
3112
|
+
* moved since stays, and so does its slot, in the same statement. False when
|
|
3113
|
+
* nothing was given back.
|
|
3114
|
+
*/
|
|
3115
|
+
releaseIfUnmoved(checkoutOfferId: string, expiresAt: Date): Promise<boolean>;
|
|
3116
|
+
/**
|
|
3117
|
+
* Marks the offer's hold, if it is live at `now`, as the slot of the
|
|
3118
|
+
* redemption that runs on `tx`. The mark never outlives the transaction: the
|
|
3119
|
+
* redemption turns the hold into its slot (`convertHandedOver`), or the
|
|
3120
|
+
* caller releases it before committing. False when the offer has no live
|
|
3121
|
+
* hold.
|
|
3122
|
+
*/
|
|
3123
|
+
handOver(checkoutOfferId: string, now: Date, tx: TransactionContext): Promise<boolean>;
|
|
3124
|
+
/**
|
|
3125
|
+
* Turns the hold of that code handed over on `tx` into a redemption's slot:
|
|
3126
|
+
* the row goes, `heldCount` drops by one and `redemptionsCount` rises by one,
|
|
3127
|
+
* whatever the code's status. False when nothing was handed over on `tx`.
|
|
3128
|
+
*/
|
|
3129
|
+
convertHandedOver(promoCodeId: string, tx: TransactionContext): Promise<boolean>;
|
|
3130
|
+
/**
|
|
3131
|
+
* Ends every hold past its expiry at `now` — of one code when `promoCodeId`
|
|
3132
|
+
* is given — and gives the slots back. A hold handed over on `tx` is left
|
|
3133
|
+
* to the conclusion running there; one handed over on another running
|
|
3134
|
+
* transaction is waited for, and is gone once that transaction ends.
|
|
3135
|
+
* Returns how many ended.
|
|
3136
|
+
*/
|
|
3137
|
+
expireDue(now: Date, promoCodeId?: string, tx?: TransactionContext): Promise<number>;
|
|
3138
|
+
}
|
|
2379
3139
|
/** Adapter for PromoCodeRedemption persistence. */
|
|
2380
3140
|
interface PromoCodeRedemptionRepository {
|
|
2381
3141
|
findBySubscription(subscriptionId: string, tx?: TransactionContext): Promise<PromoCodeRedemptionRecord | null>;
|
|
@@ -2427,6 +3187,26 @@ interface PromoRevenueDeductionAggregator {
|
|
|
2427
3187
|
|
|
2428
3188
|
type ContractLineItemKind = 'plan' | 'bundle' | 'discount';
|
|
2429
3189
|
type SubscriptionContractStatus = 'active' | 'scheduled' | 'terminated' | 'superseded';
|
|
3190
|
+
/**
|
|
3191
|
+
* The statuses a contract is looked up under when asking "what is this tenant
|
|
3192
|
+
* on right now" — `scheduled` included, because a contract that starts today
|
|
3193
|
+
* and has not been switched to `active` yet is still the one in force at its
|
|
3194
|
+
* own `effectiveFrom`.
|
|
3195
|
+
*
|
|
3196
|
+
* One list rather than one per adapter: the two adapters have to answer
|
|
3197
|
+
* `findActiveByTenantId` the same way, and a status added here must not reach
|
|
3198
|
+
* only whichever of them somebody remembered.
|
|
3199
|
+
*/
|
|
3200
|
+
/**
|
|
3201
|
+
* How many bundle versions one price lookup may name.
|
|
3202
|
+
*
|
|
3203
|
+
* One number rather than two: the server validates against it and the client
|
|
3204
|
+
* batches to stay inside it, and a client that learned the cap by receiving a
|
|
3205
|
+
* 400 would fail silently — the lookup answers with an empty map, and every
|
|
3206
|
+
* card falls back to a catalogue price the tenant may not be charged.
|
|
3207
|
+
*/
|
|
3208
|
+
declare const BUNDLE_PRICE_LOOKUP_LIMIT = 200;
|
|
3209
|
+
declare const ACTIVE_SUBSCRIPTION_CONTRACT_STATUSES: readonly SubscriptionContractStatus[];
|
|
2430
3210
|
interface ContractLineItemRecord {
|
|
2431
3211
|
id: string;
|
|
2432
3212
|
contractId: string;
|
|
@@ -2437,9 +3217,36 @@ interface ContractLineItemRecord {
|
|
|
2437
3217
|
descriptionSnapshot: string | null;
|
|
2438
3218
|
quantity: number;
|
|
2439
3219
|
unit: string | null;
|
|
3220
|
+
/**
|
|
3221
|
+
* What the line costs over one of its billing periods, `quantity` already
|
|
3222
|
+
* in it — not a unit price. A contract's totals take its lines as they
|
|
3223
|
+
* stand, each counted as often as it falls due in one period of the
|
|
3224
|
+
* contract, and are refused where they do not add up.
|
|
3225
|
+
*/
|
|
2440
3226
|
priceNet: number;
|
|
3227
|
+
/** `priceNet` with the line's share of the tax its rhythm owes. */
|
|
2441
3228
|
priceGross: number;
|
|
2442
3229
|
billingCycle: 'monthly' | 'yearly';
|
|
3230
|
+
/**
|
|
3231
|
+
* ISO 4217, as the line was booked in.
|
|
3232
|
+
*
|
|
3233
|
+
* An installation sells in one currency at a time, so this is never a
|
|
3234
|
+
* choice the line makes — it is what keeps the line meaning what it meant
|
|
3235
|
+
* after the configured currency is migrated to another one.
|
|
3236
|
+
*/
|
|
3237
|
+
currency: string;
|
|
3238
|
+
/**
|
|
3239
|
+
* The tax rate in percent that was applied, recorded rather than left in
|
|
3240
|
+
* the ratio between net and gross. That ratio is not the rate: it cannot be
|
|
3241
|
+
* reproduced for a rounded gross, cannot express an exempt or reverse-charge
|
|
3242
|
+
* line, and does not survive a rate change.
|
|
3243
|
+
*/
|
|
3244
|
+
taxRate: number;
|
|
3245
|
+
/**
|
|
3246
|
+
* The tax contained in the line — exactly `priceGross - priceNet`, so the
|
|
3247
|
+
* line cannot disagree with itself. Rounded once, when the line is written.
|
|
3248
|
+
*/
|
|
3249
|
+
taxAmount: number;
|
|
2443
3250
|
minimumTermUntil: Date | null;
|
|
2444
3251
|
featuresSnapshot: string[];
|
|
2445
3252
|
quotaEffectsSnapshot: Record<string, number>;
|
|
@@ -2452,13 +3259,47 @@ interface SubscriptionContractPriceSnapshot {
|
|
|
2452
3259
|
subtotalNet: number;
|
|
2453
3260
|
discountNet: number;
|
|
2454
3261
|
totalNet: number;
|
|
3262
|
+
/**
|
|
3263
|
+
* The tax rate this contract's total was computed at, as a percentage:
|
|
3264
|
+
* 19 means 19 %, as every tax rate in SaaSiCat is.
|
|
3265
|
+
*/
|
|
2455
3266
|
vatRate: number;
|
|
2456
3267
|
totalGross: number;
|
|
2457
3268
|
}
|
|
2458
|
-
|
|
3269
|
+
/**
|
|
3270
|
+
* The subscriber as a contract copied it on the day it was concluded.
|
|
3271
|
+
*
|
|
3272
|
+
* The invoice email is not part of it: it says how the party is reached, not
|
|
3273
|
+
* who the party is, and a contract is kept for years after an address like that
|
|
3274
|
+
* stopped mattering.
|
|
3275
|
+
*/
|
|
3276
|
+
interface ContractSubscriberParty extends LegalIdentity, PartyAddress {
|
|
3277
|
+
customerNumber: string;
|
|
3278
|
+
}
|
|
3279
|
+
/** The issuer as `config/saas.yaml` named it on the day a contract was concluded. */
|
|
3280
|
+
interface ContractIssuerParty extends LegalIdentity, PartyAddress {
|
|
3281
|
+
}
|
|
3282
|
+
/** Who a contract is between, copied when it is concluded. */
|
|
3283
|
+
interface SubscriptionContractParties {
|
|
3284
|
+
subscriberId: string;
|
|
3285
|
+
subscriber: ContractSubscriberParty;
|
|
3286
|
+
/** `null` where `config/saas.yaml` named no issuer that day. */
|
|
3287
|
+
issuer: ContractIssuerParty | null;
|
|
3288
|
+
}
|
|
3289
|
+
interface SubscriptionContractRecord extends SubscriptionContractParties {
|
|
2459
3290
|
id: string;
|
|
2460
|
-
|
|
3291
|
+
/**
|
|
3292
|
+
* The tenant the contract was concluded for, kept as a trace. The contract
|
|
3293
|
+
* belongs to its subscriber and outlives the tenant.
|
|
3294
|
+
*/
|
|
2461
3295
|
tenantId: string;
|
|
3296
|
+
/**
|
|
3297
|
+
* The parties were copied by the migration that attached contracts
|
|
3298
|
+
* concluded before subscribers existed, not on the day the contract was
|
|
3299
|
+
* concluded. Either party may have changed in between, so such a copy is
|
|
3300
|
+
* never presented as what was agreed.
|
|
3301
|
+
*/
|
|
3302
|
+
partiesMigrated: boolean;
|
|
2462
3303
|
status: SubscriptionContractStatus;
|
|
2463
3304
|
effectiveFrom: Date;
|
|
2464
3305
|
effectiveUntil: Date | null;
|
|
@@ -2475,8 +3316,12 @@ interface SubscriptionContractRecord {
|
|
|
2475
3316
|
updatedAt: Date;
|
|
2476
3317
|
}
|
|
2477
3318
|
type NewContractLineItemData = Omit<ContractLineItemRecord, 'id' | 'contractId' | 'createdAt'>;
|
|
3319
|
+
/**
|
|
3320
|
+
* A contract as a caller asks for it. The parties are not among it: the
|
|
3321
|
+
* platform copies them from the tenant's subscriber and the configuration when
|
|
3322
|
+
* the contract is written, so no caller can name a party of its own.
|
|
3323
|
+
*/
|
|
2478
3324
|
interface CreateSubscriptionContractData {
|
|
2479
|
-
projectKey: string;
|
|
2480
3325
|
tenantId: string;
|
|
2481
3326
|
status?: SubscriptionContractStatus;
|
|
2482
3327
|
effectiveFrom: Date;
|
|
@@ -2491,16 +3336,53 @@ interface CreateSubscriptionContractData {
|
|
|
2491
3336
|
termsSnapshot?: Record<string, unknown> | null;
|
|
2492
3337
|
lineItems: NewContractLineItemData[];
|
|
2493
3338
|
}
|
|
3339
|
+
/** What a repository writes: the contract as asked for, with the parties the platform copied. */
|
|
3340
|
+
interface NewSubscriptionContractData extends CreateSubscriptionContractData {
|
|
3341
|
+
parties: SubscriptionContractParties;
|
|
3342
|
+
}
|
|
2494
3343
|
interface TerminateSubscriptionContractData {
|
|
2495
3344
|
effectiveUntil: Date;
|
|
2496
|
-
|
|
3345
|
+
/**
|
|
3346
|
+
* The terminal status, or `null` to end the contract by date alone.
|
|
3347
|
+
*
|
|
3348
|
+
* `findActiveByTenantId` already asks its question as a window —
|
|
3349
|
+
* `effectiveFrom <= asOf` and `effectiveUntil` null or after it — so a
|
|
3350
|
+
* contract given an end in the FUTURE is found until that moment and not
|
|
3351
|
+
* afterwards, with no scheduled job to flip anything.
|
|
3352
|
+
*
|
|
3353
|
+
* Writing a terminal status instead makes the contract disappear from that
|
|
3354
|
+
* lookup at once, which for a cancellation declared months ahead removes an
|
|
3355
|
+
* agreement the customer is still under. Null is how a caller says "it ends
|
|
3356
|
+
* then", and a status is how it says "it is over now".
|
|
3357
|
+
*/
|
|
3358
|
+
status: Extract<SubscriptionContractStatus, 'terminated' | 'superseded'> | null;
|
|
2497
3359
|
}
|
|
2498
3360
|
interface SubscriptionContractFilter {
|
|
2499
|
-
projectKey?: string;
|
|
2500
3361
|
tenantId?: string;
|
|
2501
3362
|
status?: SubscriptionContractStatus;
|
|
2502
3363
|
asOf?: Date;
|
|
2503
3364
|
}
|
|
3365
|
+
/** One contract still running, and the issuer it was concluded under. */
|
|
3366
|
+
interface RunningContractIssuer {
|
|
3367
|
+
id: string;
|
|
3368
|
+
/** The tenant it was concluded for, kept as a trace. */
|
|
3369
|
+
tenantId: string;
|
|
3370
|
+
/**
|
|
3371
|
+
* The legal name on the issuer copy, or `null` where the contract names no
|
|
3372
|
+
* issuer — it was concluded while `config/saas.yaml` named none, or its
|
|
3373
|
+
* party copy was made by the migration that attached contracts concluded
|
|
3374
|
+
* before subscribers existed.
|
|
3375
|
+
*/
|
|
3376
|
+
issuerLegalName: string | null;
|
|
3377
|
+
effectiveFrom: Date;
|
|
3378
|
+
}
|
|
3379
|
+
/** How many contracts are concluded and not yet over, and the first few of them. */
|
|
3380
|
+
interface RunningContractIssuers {
|
|
3381
|
+
/** All of them, whether or not the list below holds them all. */
|
|
3382
|
+
total: number;
|
|
3383
|
+
/** At most the limit the caller asked for, oldest first. */
|
|
3384
|
+
contracts: RunningContractIssuer[];
|
|
3385
|
+
}
|
|
2504
3386
|
interface InvoiceLineItemSnapshot {
|
|
2505
3387
|
sourceContractLineItemId: string;
|
|
2506
3388
|
sourceKey: string;
|
|
@@ -2513,12 +3395,14 @@ interface InvoiceLineItemSnapshot {
|
|
|
2513
3395
|
priceNet: number;
|
|
2514
3396
|
priceGross: number;
|
|
2515
3397
|
billingCycle: 'monthly' | 'yearly';
|
|
3398
|
+
currency: string;
|
|
3399
|
+
taxRate: number;
|
|
3400
|
+
taxAmount: number;
|
|
2516
3401
|
minimumTermUntil: Date | null;
|
|
2517
3402
|
metadata: Record<string, unknown> | null;
|
|
2518
3403
|
}
|
|
2519
3404
|
interface SubscriptionContractInvoiceSnapshot {
|
|
2520
3405
|
contractId: string;
|
|
2521
|
-
projectKey: string;
|
|
2522
3406
|
tenantId: string;
|
|
2523
3407
|
originalOfferId: string | null;
|
|
2524
3408
|
currency: string;
|
|
@@ -2533,6 +3417,100 @@ interface SubscriptionContractInvoiceSnapshot {
|
|
|
2533
3417
|
lineItems: InvoiceLineItemSnapshot[];
|
|
2534
3418
|
}
|
|
2535
3419
|
|
|
3420
|
+
/** How a subscriber is reached, which may change at any time. */
|
|
3421
|
+
interface SubscriberContact extends PartyAddress {
|
|
3422
|
+
/** Where invoices will be sent. */
|
|
3423
|
+
invoiceEmail: string | null;
|
|
3424
|
+
}
|
|
3425
|
+
/** A subscriber's master data, as it stands. */
|
|
3426
|
+
type SubscriberDetails = LegalIdentity & SubscriberContact;
|
|
3427
|
+
/**
|
|
3428
|
+
* What a subscriber is created with.
|
|
3429
|
+
*
|
|
3430
|
+
* Only the legal name is required. The address, the tax identifiers and the
|
|
3431
|
+
* invoice email stay optional until sign-up asks for them and invoicing
|
|
3432
|
+
* requires them; an absent or blank value is recorded as unknown.
|
|
3433
|
+
*/
|
|
3434
|
+
type NewSubscriberDetails = Pick<SubscriberDetails, 'legalName'> & Partial<Omit<SubscriberDetails, 'legalName'>>;
|
|
3435
|
+
interface SubscriberRecord extends SubscriberDetails {
|
|
3436
|
+
id: string;
|
|
3437
|
+
/**
|
|
3438
|
+
* Assigned when the subscriber is created and never changed: the number,
|
|
3439
|
+
* counted per installation, behind the prefix configured at that moment.
|
|
3440
|
+
*/
|
|
3441
|
+
customerNumber: string;
|
|
3442
|
+
/** The tenant this subscriber is live for, or `null` once it has none. */
|
|
3443
|
+
tenantId: string | null;
|
|
3444
|
+
/**
|
|
3445
|
+
* Created by the migration that gave every existing tenant its subscriber,
|
|
3446
|
+
* from the application's own tenant record rather than from what a customer
|
|
3447
|
+
* entered.
|
|
3448
|
+
*/
|
|
3449
|
+
migrated: boolean;
|
|
3450
|
+
createdAt: Date;
|
|
3451
|
+
updatedAt: Date;
|
|
3452
|
+
}
|
|
3453
|
+
/** What a repository writes for a new subscriber, every detail already settled. */
|
|
3454
|
+
interface CreateSubscriberData extends SubscriberDetails {
|
|
3455
|
+
/** The tenant the subscriber is created for and live with. */
|
|
3456
|
+
tenantId: string;
|
|
3457
|
+
/** Put in front of the assigned number; empty for the number alone. */
|
|
3458
|
+
customerNumberPrefix: string;
|
|
3459
|
+
}
|
|
3460
|
+
/** Contact details to change; a member left out keeps its value. */
|
|
3461
|
+
type SubscriberContactChange = Partial<SubscriberContact>;
|
|
3462
|
+
/**
|
|
3463
|
+
* A change to a subscriber's legal identity, and what the operator declares it
|
|
3464
|
+
* to be.
|
|
3465
|
+
*
|
|
3466
|
+
* SaaSiCat cannot tell a misspelt name from another company taking over, so the
|
|
3467
|
+
* operator says which: `correction` for the same legal entity — a typo, a wrong
|
|
3468
|
+
* tax identifier, a change of name that entity went through — and `takeover`
|
|
3469
|
+
* for another one, which is a transfer rather than an edit and is refused.
|
|
3470
|
+
*/
|
|
3471
|
+
interface SubscriberIdentityCorrection {
|
|
3472
|
+
kind: 'correction' | 'takeover';
|
|
3473
|
+
legalName?: string;
|
|
3474
|
+
vatId?: string | null;
|
|
3475
|
+
taxNumber?: string | null;
|
|
3476
|
+
/** Why the identity is corrected. Part of the record. */
|
|
3477
|
+
reason: string;
|
|
3478
|
+
/** Who corrects it, as an actor tag the audit log would write. */
|
|
3479
|
+
correctedBy: string;
|
|
3480
|
+
}
|
|
3481
|
+
/** Identity values by field, holding only the fields a correction changed. */
|
|
3482
|
+
type SubscriberIdentityValues = Partial<LegalIdentity>;
|
|
3483
|
+
/** What a correction replaces and what it writes, holding only the fields that move. */
|
|
3484
|
+
interface SubscriberIdentityDelta {
|
|
3485
|
+
previous: SubscriberIdentityValues;
|
|
3486
|
+
corrected: SubscriberIdentityValues;
|
|
3487
|
+
}
|
|
3488
|
+
/** What a repository writes for a correction the service has accepted. */
|
|
3489
|
+
interface SubscriberCorrectionData {
|
|
3490
|
+
/** The new values; a field equal to what is stored is not recorded. */
|
|
3491
|
+
corrected: SubscriberIdentityValues;
|
|
3492
|
+
reason: string;
|
|
3493
|
+
correctedBy: string;
|
|
3494
|
+
correctedAt: Date;
|
|
3495
|
+
}
|
|
3496
|
+
/** One correction of a subscriber's legal identity, as it was recorded. */
|
|
3497
|
+
interface SubscriberCorrectionRecord {
|
|
3498
|
+
id: string;
|
|
3499
|
+
subscriberId: string;
|
|
3500
|
+
/** The values the correction replaced. */
|
|
3501
|
+
previous: SubscriberIdentityValues;
|
|
3502
|
+
/** The values it wrote. */
|
|
3503
|
+
corrected: SubscriberIdentityValues;
|
|
3504
|
+
reason: string;
|
|
3505
|
+
correctedBy: string;
|
|
3506
|
+
correctedAt: Date;
|
|
3507
|
+
}
|
|
3508
|
+
/** The outcome of writing a correction: nothing is recorded when no value moved. */
|
|
3509
|
+
interface SubscriberCorrectionResult {
|
|
3510
|
+
subscriber: SubscriberRecord;
|
|
3511
|
+
correction: SubscriberCorrectionRecord | null;
|
|
3512
|
+
}
|
|
3513
|
+
|
|
2536
3514
|
/**
|
|
2537
3515
|
* Snapshot form of a `Subscription` row for the EntitlementService
|
|
2538
3516
|
* computation. The consumer maps its Prisma structure onto this form.
|
|
@@ -2552,6 +3530,24 @@ interface SubscriptionRecord {
|
|
|
2552
3530
|
} | null;
|
|
2553
3531
|
planVersionId: string;
|
|
2554
3532
|
planVersion: PlanVersionRecord;
|
|
3533
|
+
/**
|
|
3534
|
+
* When a cancellation was declared, and when it takes effect.
|
|
3535
|
+
*
|
|
3536
|
+
* Required, and required together, because entitlement resolution ends a
|
|
3537
|
+
* subscription by reading them: without the second date it cannot tell a
|
|
3538
|
+
* subscription that ends next January from one that ended last January, and
|
|
3539
|
+
* it grants the latter everything. Nothing else in the platform would
|
|
3540
|
+
* notice — no repository filters a cancelled subscription out, and stopping
|
|
3541
|
+
* the billing period is a different decision from ending what a tenant may
|
|
3542
|
+
* do.
|
|
3543
|
+
*
|
|
3544
|
+
* `null` on both means no cancellation. On a row written before the two
|
|
3545
|
+
* fields separated, `canceledAt` carries the effective date and
|
|
3546
|
+
* `canceledEffectiveAt` is genuinely null; every reader in the platform
|
|
3547
|
+
* applies `canceledEffectiveAt ?? canceledAt` for that reason.
|
|
3548
|
+
*/
|
|
3549
|
+
canceledAt: Date | null;
|
|
3550
|
+
canceledEffectiveAt: Date | null;
|
|
2555
3551
|
}
|
|
2556
3552
|
/** Snapshot of a `PlanVersion` row. */
|
|
2557
3553
|
interface PlanVersionRecord {
|
|
@@ -2608,11 +3604,10 @@ interface SubscriptionRepository {
|
|
|
2608
3604
|
countByBundleVersionId?(bundleVersionId: string): Promise<number>;
|
|
2609
3605
|
/**
|
|
2610
3606
|
* Counts active subscriptions (status `ACTIVE` or `TRIAL`) per plan key,
|
|
2611
|
-
* platform-wide across
|
|
2612
|
-
*
|
|
3607
|
+
* platform-wide across every tenant — feeds the tenant column of the
|
|
3608
|
+
* SuperAdmin plan list (`GET /admin/catalog/plans/tenant-counts`).
|
|
2613
3609
|
* Cross-version: counts the plan, not a single PlanVersion
|
|
2614
|
-
* (subscriptions on superseded versions are included).
|
|
2615
|
-
* informational for single-project consumers.
|
|
3610
|
+
* (subscriptions on superseded versions are included).
|
|
2616
3611
|
*
|
|
2617
3612
|
* Returns a map `planKey → count`; plans without an active subscription
|
|
2618
3613
|
* are missing (UI defaults to 0). Platform-wide count across all tenants →
|
|
@@ -2620,7 +3615,7 @@ interface SubscriptionRepository {
|
|
|
2620
3615
|
*
|
|
2621
3616
|
* Optional — if not implemented, the tenant column stays 0.
|
|
2622
3617
|
*/
|
|
2623
|
-
countActiveByPlanKey?(
|
|
3618
|
+
countActiveByPlanKey?(): Promise<Record<string, number>>;
|
|
2624
3619
|
}
|
|
2625
3620
|
/**
|
|
2626
3621
|
* Adapter for the `subscription_bundles` junction.
|
|
@@ -2679,8 +3674,89 @@ interface SubscriptionContractRepository {
|
|
|
2679
3674
|
* instead of drawing an extra pool connection (starvation guard, #70).
|
|
2680
3675
|
*/
|
|
2681
3676
|
findActiveByTenantId(tenantId: string, asOf?: Date, tx?: TransactionContext): Promise<SubscriptionContractRecord | null>;
|
|
2682
|
-
|
|
3677
|
+
/**
|
|
3678
|
+
* Writes the contract with the parties it names. With `tx`, the contract is
|
|
3679
|
+
* written on that transaction and undone with it.
|
|
3680
|
+
*/
|
|
3681
|
+
create(data: NewSubscriptionContractData, tx?: TransactionContext): Promise<SubscriptionContractRecord>;
|
|
3682
|
+
/**
|
|
3683
|
+
* The contract concluded from a checkout offer (`originalOfferId`), or
|
|
3684
|
+
* `null` when none was. An offer is consumed once and so yields one
|
|
3685
|
+
* contract (`SC-MKT-017`); where an application's own path wrote two, the
|
|
3686
|
+
* earliest is returned, so the answer does not depend on read order.
|
|
3687
|
+
*/
|
|
3688
|
+
findByOriginalOfferId(offerId: string, tx?: TransactionContext): Promise<SubscriptionContractRecord | null>;
|
|
2683
3689
|
terminate(contractId: string, data: TerminateSubscriptionContractData): Promise<SubscriptionContractRecord>;
|
|
3690
|
+
/**
|
|
3691
|
+
* The contracts concluded and not yet over, with the issuer each was
|
|
3692
|
+
* concluded under: how many there are, and the first `limit` of them,
|
|
3693
|
+
* oldest first. `limit` caps the list and not the count, so a start refused
|
|
3694
|
+
* over a changed issuer identity says how many contracts it means before it
|
|
3695
|
+
* names any of them.
|
|
3696
|
+
*
|
|
3697
|
+
* Running means `active` or `scheduled` AND not ended at `asOf` — the same
|
|
3698
|
+
* window `findActiveByTenantId` uses on its upper end, and for the same
|
|
3699
|
+
* reason. Status alone is not enough: an ordinary cancellation lands at the
|
|
3700
|
+
* term end and writes only `effectiveUntil`, leaving the status where it
|
|
3701
|
+
* was, and nothing flips it when that day arrives. Counting by status would
|
|
3702
|
+
* therefore report every customer who ever left as still running — and
|
|
3703
|
+
* because the list is oldest first, the ones it names would be exactly the
|
|
3704
|
+
* expired ones.
|
|
3705
|
+
*
|
|
3706
|
+
* The window is open at the bottom on purpose: a contract that starts next
|
|
3707
|
+
* month is concluded, its party copy is fixed, and it will be invoiced under
|
|
3708
|
+
* the issuer it names.
|
|
3709
|
+
*
|
|
3710
|
+
* Platform-wide: unlike every other read here it is anchored by no tenant,
|
|
3711
|
+
* no contract and no offer, and a start makes it before anything is served.
|
|
3712
|
+
* An implementation on a tenant-scoped client must count RLS-exempt, as
|
|
3713
|
+
* `countActiveByPlanKey` must — the platform wraps the call in
|
|
3714
|
+
* `RlsBypassPort`, and one that answers with the caller's tenant scope
|
|
3715
|
+
* instead returns nothing at a boot, where there is no tenant. The
|
|
3716
|
+
* persistence contract runs with no policy forced, so it cannot catch that
|
|
3717
|
+
* for you.
|
|
3718
|
+
*
|
|
3719
|
+
* `limit` may be `0`, and a caller that wants only the count passes it:
|
|
3720
|
+
* `total` is exact whatever the limit, so nothing has to come back for it.
|
|
3721
|
+
* Zero means zero — an implementation that reads a falsy limit as "no limit"
|
|
3722
|
+
* returns every running contract to a caller asking for none, which is the
|
|
3723
|
+
* one shape of this method that gets slower the more an installation sells.
|
|
3724
|
+
* The executable contract asks for `0`.
|
|
3725
|
+
*/
|
|
3726
|
+
listRunningIssuers(limit: number, asOf?: Date): Promise<RunningContractIssuers>;
|
|
3727
|
+
}
|
|
3728
|
+
/**
|
|
3729
|
+
* The parties contracts are concluded with, their link to the tenant they are
|
|
3730
|
+
* live for, and the corrections of their legal identity.
|
|
3731
|
+
*
|
|
3732
|
+
* A subscriber has at most one live tenant and a tenant at most one live
|
|
3733
|
+
* subscriber; the database holds both, so two callers creating one for the
|
|
3734
|
+
* same tenant at once end with one.
|
|
3735
|
+
*/
|
|
3736
|
+
interface SubscriberRepository {
|
|
3737
|
+
/**
|
|
3738
|
+
* Creates a subscriber, assigns its customer number, and makes it the
|
|
3739
|
+
* tenant's live subscriber — all or nothing. `null` when the tenant already
|
|
3740
|
+
* has a live subscriber, in which case nothing is written and the caller's
|
|
3741
|
+
* transaction stays usable.
|
|
3742
|
+
*/
|
|
3743
|
+
createForTenant(data: CreateSubscriberData, tx?: TransactionContext): Promise<SubscriberRecord | null>;
|
|
3744
|
+
findById(subscriberId: string, tx?: TransactionContext): Promise<SubscriberRecord | null>;
|
|
3745
|
+
/** The subscriber live for this tenant, or `null` when it has none. */
|
|
3746
|
+
findByTenantId(tenantId: string, tx?: TransactionContext): Promise<SubscriberRecord | null>;
|
|
3747
|
+
/** Writes the members given and keeps the rest; `null` when no such subscriber exists. */
|
|
3748
|
+
updateContact(subscriberId: string, change: SubscriberContactChange, tx?: TransactionContext): Promise<SubscriberRecord | null>;
|
|
3749
|
+
/**
|
|
3750
|
+
* Writes a correction of the legal identity and records it, in one step:
|
|
3751
|
+
* the subscriber is read and changed under a lock, so the values recorded
|
|
3752
|
+
* as replaced are the ones this write replaced even when two corrections
|
|
3753
|
+
* arrive at once. A field whose stored value already equals the corrected
|
|
3754
|
+
* one is left out of the record, and when none differs nothing is written
|
|
3755
|
+
* and `correction` is `null`. `null` when no such subscriber exists.
|
|
3756
|
+
*/
|
|
3757
|
+
correctIdentity(subscriberId: string, data: SubscriberCorrectionData, tx?: TransactionContext): Promise<SubscriberCorrectionResult | null>;
|
|
3758
|
+
/** Every correction of this subscriber, the latest first. */
|
|
3759
|
+
listCorrections(subscriberId: string): Promise<SubscriberCorrectionRecord[]>;
|
|
2684
3760
|
}
|
|
2685
3761
|
/**
|
|
2686
3762
|
* Display form of a subscription for the tenant self-service UI.
|
|
@@ -2712,6 +3788,40 @@ interface SubscriptionUsageRecord {
|
|
|
2712
3788
|
/** Current period window — for proration and change-effective date. */
|
|
2713
3789
|
currentPeriodStart: Date | null;
|
|
2714
3790
|
currentPeriodEnd: Date | null;
|
|
3791
|
+
/**
|
|
3792
|
+
* End of what was committed to, which the period end need not equal.
|
|
3793
|
+
*
|
|
3794
|
+
* The cancellation rules measure against this: a subscription cancelled
|
|
3795
|
+
* inside its term keeps running until the term ends, not until the period
|
|
3796
|
+
* does. Null on a trial, and on any subscription written before the field
|
|
3797
|
+
* existed — readers treat that as "the period end is the answer".
|
|
3798
|
+
*/
|
|
3799
|
+
minimumTermUntil?: Date | null;
|
|
3800
|
+
/**
|
|
3801
|
+
* The day of the month the subscription is billed on, 1–31.
|
|
3802
|
+
*
|
|
3803
|
+
* Read by the cancellation rules: a declaration after the notice window
|
|
3804
|
+
* lands one period past the term end, and that step has to measure from the
|
|
3805
|
+
* billing day rather than from a term end that may already have been
|
|
3806
|
+
* clamped by a short month.
|
|
3807
|
+
*
|
|
3808
|
+
* Optional, because an adapter that does not store the column keeps today's
|
|
3809
|
+
* behaviour — the step then takes its day from the term end, which is
|
|
3810
|
+
* correct except in the month after a clamp.
|
|
3811
|
+
*/
|
|
3812
|
+
billingAnchorDay?: number | null;
|
|
3813
|
+
/**
|
|
3814
|
+
* When a cancellation was declared, and when it lands.
|
|
3815
|
+
*
|
|
3816
|
+
* Required for the reason the same pair is required on
|
|
3817
|
+
* `SubscriptionRecord`: the tenant billing route reads them to refuse a
|
|
3818
|
+
* plan change on a subscription that has ended, and a record that omits
|
|
3819
|
+
* them answers "not cancelled" — so the change is applied and prorated
|
|
3820
|
+
* while entitlement resolution, which reads a record that does carry them,
|
|
3821
|
+
* grants nothing.
|
|
3822
|
+
*/
|
|
3823
|
+
canceledAt: Date | null;
|
|
3824
|
+
canceledEffectiveAt: Date | null;
|
|
2715
3825
|
pendingPlan: string | null;
|
|
2716
3826
|
pendingBillingCycle: string | null;
|
|
2717
3827
|
pendingEffectiveAt: Date | null;
|
|
@@ -2787,12 +3897,29 @@ interface ImmediatePlanChangeInput {
|
|
|
2787
3897
|
* change, or target package without trial). A `Date` is persisted.
|
|
2788
3898
|
*/
|
|
2789
3899
|
trialEndsAt?: Date | null;
|
|
3900
|
+
/**
|
|
3901
|
+
* `canceledAt` as the caller read it, so the write can claim the row only
|
|
3902
|
+
* while that is still true.
|
|
3903
|
+
*
|
|
3904
|
+
* Three of the plan route's decisions depend on the cancellation — whether
|
|
3905
|
+
* the change is refused at all, whether the billing cycle may move, and
|
|
3906
|
+
* whether a fresh period is opened — and a read and a write are two
|
|
3907
|
+
* moments. A cancellation declared in between made every one of them answer
|
|
3908
|
+
* about a state that no longer existed, and the write went ahead anyway: a
|
|
3909
|
+
* plan term recorded past the date the subscription ends.
|
|
3910
|
+
*
|
|
3911
|
+
* `null` is a value here rather than an absence. It claims a row that has
|
|
3912
|
+
* no cancellation, and loses against one that has acquired one.
|
|
3913
|
+
*/
|
|
3914
|
+
expectedCanceledAt: Date | null;
|
|
2790
3915
|
}
|
|
2791
3916
|
/** Input for `schedulePlanChange` (change at period end). */
|
|
2792
3917
|
interface ScheduledPlanChangeInput {
|
|
2793
3918
|
pendingPlan: string;
|
|
2794
3919
|
pendingBillingCycle: string;
|
|
2795
3920
|
pendingEffectiveAt: Date;
|
|
3921
|
+
/** See `ImmediatePlanChangeInput.expectedCanceledAt`. */
|
|
3922
|
+
expectedCanceledAt: Date | null;
|
|
2796
3923
|
}
|
|
2797
3924
|
/**
|
|
2798
3925
|
* Input for `applyOnboardingSelection`. Plan-change fields that the
|
|
@@ -2801,6 +3928,12 @@ interface ScheduledPlanChangeInput {
|
|
|
2801
3928
|
interface ApplyOnboardingSelectionInput {
|
|
2802
3929
|
planId: string;
|
|
2803
3930
|
cycle: string;
|
|
3931
|
+
/**
|
|
3932
|
+
* See `ImmediatePlanChangeInput.expectedCanceledAt`. The atomic path needs
|
|
3933
|
+
* it for the same reason the sequential one does: without it, the preferred
|
|
3934
|
+
* implementation is the one where the race stays open.
|
|
3935
|
+
*/
|
|
3936
|
+
expectedCanceledAt: Date | null;
|
|
2804
3937
|
/** For TRIAL → null, otherwise period start from `initialPeriodWindow`. */
|
|
2805
3938
|
periodStart: Date | null;
|
|
2806
3939
|
periodEnd: Date | null;
|
|
@@ -2817,6 +3950,12 @@ interface ApplyOnboardingSelectionResult {
|
|
|
2817
3950
|
subscriptionId: string;
|
|
2818
3951
|
/** null if no redeemPromo callback was provided or the callback returned null. */
|
|
2819
3952
|
promoRedemption: PromoCodeRedemptionRecord | null;
|
|
3953
|
+
/**
|
|
3954
|
+
* False when the row's cancellation moved since the caller read it, in
|
|
3955
|
+
* which case nothing was written — including the promo redemption, which
|
|
3956
|
+
* shares the transaction.
|
|
3957
|
+
*/
|
|
3958
|
+
claimed: boolean;
|
|
2820
3959
|
}
|
|
2821
3960
|
/**
|
|
2822
3961
|
* Callback signature for promo-code redemption WITHIN the onboarding
|
|
@@ -2834,14 +3973,54 @@ type RedeemPromoInTransactionCallback = (tx: TransactionContext, subscriptionId:
|
|
|
2834
3973
|
* app-specific. The platform service calls `invalidateTenant` in the
|
|
2835
3974
|
* EntitlementService after a successful adapter call.
|
|
2836
3975
|
*/
|
|
3976
|
+
/** What `cancelSubscription` is told to write. Named so both adapters spell
|
|
3977
|
+
* the same shape once rather than each restating it. */
|
|
3978
|
+
interface CancelSubscriptionInput {
|
|
3979
|
+
canceledAt: Date;
|
|
3980
|
+
effectiveAt: Date;
|
|
3981
|
+
terminateNow: boolean;
|
|
3982
|
+
minimumTermUntil?: Date;
|
|
3983
|
+
}
|
|
3984
|
+
/** What `cancelSubscription` answers with. */
|
|
3985
|
+
interface CancelSubscriptionResult {
|
|
3986
|
+
canceledAt: Date | null;
|
|
3987
|
+
canceledEffectiveAt: Date | null;
|
|
3988
|
+
status: string;
|
|
3989
|
+
/**
|
|
3990
|
+
* True when a cancellation was already recorded and this call changed
|
|
3991
|
+
* nothing — the stored dates are returned instead.
|
|
3992
|
+
*
|
|
3993
|
+
* The caller checks first, but a check and a write are two moments, and two
|
|
3994
|
+
* requests can pass the check before either writes. Straddling a notice
|
|
3995
|
+
* deadline that costs a billing cycle: the first declaration lands on time,
|
|
3996
|
+
* the second recomputes against a later `now`, and an unconditional write
|
|
3997
|
+
* replaces the first answer with one a period further out. An
|
|
3998
|
+
* implementation therefore claims the row only while both cancellation
|
|
3999
|
+
* fields are still empty, and answers `true` here when the claim finds
|
|
4000
|
+
* nothing to claim.
|
|
4001
|
+
*/
|
|
4002
|
+
alreadyCanceled: boolean;
|
|
4003
|
+
}
|
|
2837
4004
|
interface TenantSubscriptionWritePort {
|
|
4005
|
+
/**
|
|
4006
|
+
* Whether the writes that change a plan — the immediate change and the
|
|
4007
|
+
* onboarding selection — bind the subscription's `planVersionId` to the
|
|
4008
|
+
* version they sell. A contract freeze records the bound version, so it
|
|
4009
|
+
* refuses to start beside a write that says `false`. Left out, it is taken
|
|
4010
|
+
* on trust: the platform cannot see into a write it did not ship.
|
|
4011
|
+
*/
|
|
4012
|
+
readonly bindsPlanVersion?: boolean;
|
|
2838
4013
|
/** Immediate change: set plan + cycle, clear pending fields, optionally reset the period. */
|
|
2839
4014
|
changePlanImmediate(tenantId: string, input: ImmediatePlanChangeInput): Promise<{
|
|
2840
4015
|
plan: string;
|
|
2841
4016
|
billingCycle: string;
|
|
4017
|
+
/** False when the row's cancellation moved since the caller read it. */
|
|
4018
|
+
claimed: boolean;
|
|
2842
4019
|
}>;
|
|
2843
4020
|
/** Change at period end: set pending fields. */
|
|
2844
|
-
schedulePlanChange(tenantId: string, input: ScheduledPlanChangeInput): Promise<
|
|
4021
|
+
schedulePlanChange(tenantId: string, input: ScheduledPlanChangeInput): Promise<{
|
|
4022
|
+
claimed: boolean;
|
|
4023
|
+
}>;
|
|
2845
4024
|
/**
|
|
2846
4025
|
* Marks the pending PlanVersion as accepted. Idempotent — a duplicate
|
|
2847
4026
|
* accept is a no-op. Returns `alreadyAccepted: true` if the status was
|
|
@@ -2854,13 +4033,30 @@ interface TenantSubscriptionWritePort {
|
|
|
2854
4033
|
alreadyAccepted: boolean;
|
|
2855
4034
|
}>;
|
|
2856
4035
|
/**
|
|
2857
|
-
*
|
|
2858
|
-
*
|
|
4036
|
+
* Record a cancellation. The dates are decided above this port.
|
|
4037
|
+
*
|
|
4038
|
+
* `canceledAt` is when the customer said it; `effectiveAt` is when it
|
|
4039
|
+
* lands. They differ for every ordinary cancellation, because a
|
|
4040
|
+
* subscription cancelled inside its term keeps running, keeps being billed
|
|
4041
|
+
* and keeps its entitlements until the term ends. An adapter that computed
|
|
4042
|
+
* the second from the first — which this one did, as
|
|
4043
|
+
* `immediate ? now : currentPeriodEnd` — was deciding a commercial
|
|
4044
|
+
* question in a persistence layer, and could not see the minimum term or
|
|
4045
|
+
* the notice period at all.
|
|
4046
|
+
*
|
|
4047
|
+
* `terminateNow` flips the status immediately, and is set when the
|
|
4048
|
+
* cancellation is already effective: an operator ending a contract, or the
|
|
4049
|
+
* rules finding nothing left to run — no period, no term, as on a trial.
|
|
4050
|
+
* It is never a client's request. A tenant may always declare a
|
|
4051
|
+
* cancellation and may never shorten the term they are in; what decides
|
|
4052
|
+
* this flag is the date the rules returned, not the date they asked for.
|
|
4053
|
+
*
|
|
4054
|
+
* `minimumTermUntil` extends the stored commitment, and is set only when
|
|
4055
|
+
* the cancellation itself extends it: a declaration made after the notice
|
|
4056
|
+
* deadline buys the following period. Left unset the stored term end is
|
|
4057
|
+
* unchanged, which is the ordinary case.
|
|
2859
4058
|
*/
|
|
2860
|
-
cancelSubscription(tenantId: string,
|
|
2861
|
-
canceledAt: Date | null;
|
|
2862
|
-
status: string;
|
|
2863
|
-
}>;
|
|
4059
|
+
cancelSubscription(tenantId: string, input: CancelSubscriptionInput): Promise<CancelSubscriptionResult>;
|
|
2864
4060
|
/**
|
|
2865
4061
|
* Atomic onboarding creation: sets plan + cycle + period window
|
|
2866
4062
|
* AND optionally calls a promo-redeem callback — all in a
|
|
@@ -2883,6 +4079,8 @@ interface PlanVersionRepository {
|
|
|
2883
4079
|
*
|
|
2884
4080
|
* Note: ignores `validFrom`/`validUntil`. For time-aware
|
|
2885
4081
|
* resolution (onboarding, plan fallback for TRIAL) use `findActive`.
|
|
4082
|
+
*
|
|
4083
|
+
* A plan key no plan has finds `null`, not an error.
|
|
2886
4084
|
*/
|
|
2887
4085
|
findLatestLive(planId: string, tx?: TransactionContext): Promise<PlanVersionRecord | null>;
|
|
2888
4086
|
/**
|
|
@@ -2895,6 +4093,8 @@ interface PlanVersionRepository {
|
|
|
2895
4093
|
* return the highest `validFrom`, explicitly ordering null start dates
|
|
2896
4094
|
* last as a legacy fallback. Adapters without validity columns may omit
|
|
2897
4095
|
* the method (consumers fall back to `findLatestLive`).
|
|
4096
|
+
*
|
|
4097
|
+
* A plan key no plan has finds `null`, not an error.
|
|
2898
4098
|
*/
|
|
2899
4099
|
findActive?(planId: string, asOf?: Date, tx?: TransactionContext): Promise<PlanVersionRecord | null>;
|
|
2900
4100
|
}
|
|
@@ -3088,23 +4288,31 @@ interface AdminResourcesPort {
|
|
|
3088
4288
|
}
|
|
3089
4289
|
/**
|
|
3090
4290
|
* Read adapter for the current AdminManifest. The consumer implementation
|
|
3091
|
-
* delegates to its `AdminManifestService.getManifest()
|
|
3092
|
-
*
|
|
4291
|
+
* delegates to its `AdminManifestService.getManifest()`, which reads the plan
|
|
4292
|
+
* catalogue on every call and therefore answers asynchronously. The platform
|
|
4293
|
+
* CLI uses this for `<app> manifest dump|hash|check` etc.
|
|
3093
4294
|
*/
|
|
3094
4295
|
interface ManifestAccessPort {
|
|
3095
|
-
getManifest(): AdminManifest
|
|
4296
|
+
getManifest(): Promise<AdminManifest>;
|
|
3096
4297
|
/** Optional: forces a rebuild from the contributions (e.g. after code reload). */
|
|
3097
|
-
rebuild?(): AdminManifest
|
|
4298
|
+
rebuild?(): Promise<AdminManifest>;
|
|
3098
4299
|
}
|
|
3099
4300
|
/**
|
|
3100
4301
|
* Adapter for the RLS bypass context. Platform code calls `runWithBypass`,
|
|
3101
4302
|
* the consumer implementation triggers the Postgres session variable
|
|
3102
4303
|
* (`set_config('app.bypass_rls', 'true', true)`) or the equivalent in
|
|
3103
|
-
* Django/other stacks. The execution context lives for
|
|
3104
|
-
*
|
|
4304
|
+
* Django/other stacks. The execution context lives for the call it wraps
|
|
4305
|
+
* (AsyncLocalStorage / `contextvars` etc.).
|
|
3105
4306
|
*
|
|
3106
4307
|
* SuperAdmin operations are platform-wide without tenant scope — without
|
|
3107
4308
|
* bypass, all RLS-protected reads would come back empty.
|
|
4309
|
+
*
|
|
4310
|
+
* **Not only inside a request.** The platform's boot checks read platform-wide
|
|
4311
|
+
* before anything is served: an implementation that reaches for a
|
|
4312
|
+
* request-scoped connection, or that asserts a request context is open, fails
|
|
4313
|
+
* at start rather than where it is called. Wrap the call the way it is given —
|
|
4314
|
+
* `AsyncLocalStorage.run` is the shape both shipped adapters use, and it is as
|
|
4315
|
+
* good at boot as it is in a request.
|
|
3108
4316
|
*/
|
|
3109
4317
|
interface RlsBypassPort {
|
|
3110
4318
|
runWithBypass<T>(fn: () => Promise<T>): Promise<T>;
|
|
@@ -3112,7 +4320,6 @@ interface RlsBypassPort {
|
|
|
3112
4320
|
|
|
3113
4321
|
/** Filter for `PlanRepository.list()`. */
|
|
3114
4322
|
interface PlanListFilter {
|
|
3115
|
-
projectKey: string;
|
|
3116
4323
|
/** Exclude soft-deleted plans — default `true`. */
|
|
3117
4324
|
excludeDeleted?: boolean;
|
|
3118
4325
|
/**
|
|
@@ -3146,7 +4353,7 @@ interface PlanListFilter {
|
|
|
3146
4353
|
interface PlanRepository {
|
|
3147
4354
|
list(filter: PlanListFilter): Promise<PlanRow[]>;
|
|
3148
4355
|
findById(planId: string): Promise<PlanRow | null>;
|
|
3149
|
-
findByKey(
|
|
4356
|
+
findByKey(planKey: string): Promise<PlanRow | null>;
|
|
3150
4357
|
create(data: CreatePlanData): Promise<PlanRow>;
|
|
3151
4358
|
update(planId: string, data: UpdatePlanData): Promise<PlanRow>;
|
|
3152
4359
|
/** Sets `deletedAt` to NOW(); soft-deleted plans are filtered from `list` by default. */
|
|
@@ -3247,10 +4454,24 @@ interface PlanRepository {
|
|
|
3247
4454
|
}
|
|
3248
4455
|
/** Filter for `BundleRepository.list()`. */
|
|
3249
4456
|
interface BundleListFilter {
|
|
3250
|
-
projectKey: string;
|
|
3251
4457
|
/** Exclude soft-deleted bundles — default `true`. */
|
|
3252
4458
|
excludeDeleted?: boolean;
|
|
3253
4459
|
}
|
|
4460
|
+
/**
|
|
4461
|
+
* What publishing a bundle draft records.
|
|
4462
|
+
*
|
|
4463
|
+
* Named because it was written out three times — the port, and each adapter's
|
|
4464
|
+
* implementation of it — and a signature restated is a contract restated: the
|
|
4465
|
+
* copies can drift, and nothing but a reader would notice.
|
|
4466
|
+
*/
|
|
4467
|
+
interface PublishBundleVersionMeta {
|
|
4468
|
+
publishedByUserId: string | null;
|
|
4469
|
+
publishedChanges: VersionChange[];
|
|
4470
|
+
nonRegressive: boolean;
|
|
4471
|
+
/** Required — validated by the service before the repository call. */
|
|
4472
|
+
validFrom: Date;
|
|
4473
|
+
validUntil: Date | null;
|
|
4474
|
+
}
|
|
3254
4475
|
/**
|
|
3255
4476
|
* Adapter for `Bundle` + `BundleVersion` persistence. Consumers implement
|
|
3256
4477
|
* this against their Prisma tables (`bundles` + `bundle_versions`).
|
|
@@ -3269,7 +4490,7 @@ interface BundleListFilter {
|
|
|
3269
4490
|
interface BundleRepository {
|
|
3270
4491
|
list(filter: BundleListFilter): Promise<BundleRow[]>;
|
|
3271
4492
|
findById(bundleId: string): Promise<BundleRow | null>;
|
|
3272
|
-
findByKey(
|
|
4493
|
+
findByKey(bundleKey: string): Promise<BundleRow | null>;
|
|
3273
4494
|
create(data: CreateBundleData): Promise<BundleRow>;
|
|
3274
4495
|
update(bundleId: string, data: UpdateBundleData): Promise<BundleRow>;
|
|
3275
4496
|
/** Sets `deletedAt` to NOW(); soft-deleted bundles are filtered from `list` by default. */
|
|
@@ -3328,14 +4549,7 @@ interface BundleRepository {
|
|
|
3328
4549
|
* if the predecessor carries a `validUntil` — the adapter only
|
|
3329
4550
|
* persists, it does not validate again.
|
|
3330
4551
|
*/
|
|
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>;
|
|
4552
|
+
publishDraft(versionId: string, publishMeta: PublishBundleVersionMeta, tx?: TransactionContext): Promise<BundleVersionRow>;
|
|
3339
4553
|
/**
|
|
3340
4554
|
* Hard-discards a draft version (`publishedAt === null`) from the DB.
|
|
3341
4555
|
* Throws if the version was already published — published versions
|
|
@@ -3373,7 +4587,6 @@ interface MarketingProjectionRepository {
|
|
|
3373
4587
|
}
|
|
3374
4588
|
/** Upsert input for a capability from the discovery sync. */
|
|
3375
4589
|
interface UpsertCapabilityEntryData {
|
|
3376
|
-
projectKey: string;
|
|
3377
4590
|
capabilityKey: string;
|
|
3378
4591
|
label: string;
|
|
3379
4592
|
description: string | null;
|
|
@@ -3390,7 +4603,6 @@ interface UpsertCapabilityEntryData {
|
|
|
3390
4603
|
}
|
|
3391
4604
|
/** Upsert input for a feature from the discovery sync. */
|
|
3392
4605
|
interface UpsertFeatureEntryData {
|
|
3393
|
-
projectKey: string;
|
|
3394
4606
|
featureKey: string;
|
|
3395
4607
|
label: string;
|
|
3396
4608
|
description: string | null;
|
|
@@ -3404,7 +4616,6 @@ interface UpsertFeatureEntryData {
|
|
|
3404
4616
|
}
|
|
3405
4617
|
/** Upsert input for a quota from the discovery sync. */
|
|
3406
4618
|
interface UpsertQuotaEntryData {
|
|
3407
|
-
projectKey: string;
|
|
3408
4619
|
quotaKey: string;
|
|
3409
4620
|
label: string;
|
|
3410
4621
|
description: string | null;
|
|
@@ -3434,7 +4645,7 @@ interface SetCatalogEntryReviewData {
|
|
|
3434
4645
|
* Prisma tables.
|
|
3435
4646
|
*
|
|
3436
4647
|
* Binding:
|
|
3437
|
-
* - `upsert*` matches on
|
|
4648
|
+
* - `upsert*` matches on `<key>` and leaves `i18n`,
|
|
3438
4649
|
* `sortOrder`, `createdAt` as well as the approval fields (`approvedAt`/
|
|
3439
4650
|
* `approvedBy`/`approvedSignature`) **untouched** on an update —
|
|
3440
4651
|
* only the code-derived fields + the status (resolved by the service)
|
|
@@ -3450,7 +4661,7 @@ interface CatalogEntryRepository {
|
|
|
3450
4661
|
upsertCapability(data: UpsertCapabilityEntryData): Promise<CapabilityCatalogEntryRow>;
|
|
3451
4662
|
upsertFeature(data: UpsertFeatureEntryData): Promise<FeatureCatalogEntryRow>;
|
|
3452
4663
|
upsertQuota(data: UpsertQuotaEntryData): Promise<QuotaCatalogEntryRow>;
|
|
3453
|
-
retireMissing(
|
|
4664
|
+
retireMissing(type: 'capability' | 'feature' | 'quota', presentKeys: string[]): Promise<number>;
|
|
3454
4665
|
/**
|
|
3455
4666
|
* Sets or clears the successor pointer of a feature/quota
|
|
3456
4667
|
* (#39). The sync calls this when a key disappears from the snapshot
|
|
@@ -3459,17 +4670,17 @@ interface CatalogEntryRepository {
|
|
|
3459
4670
|
* adapters without a `successor_key` column omit the methods, and the sync
|
|
3460
4671
|
* then skips the pointers with a warn log.
|
|
3461
4672
|
*/
|
|
3462
|
-
setFeatureSuccessor?(
|
|
3463
|
-
setQuotaSuccessor?(
|
|
3464
|
-
findFeature(
|
|
3465
|
-
findQuota(
|
|
3466
|
-
setFeatureReview(
|
|
3467
|
-
setQuotaReview(
|
|
3468
|
-
setFeatureI18n(
|
|
3469
|
-
setQuotaI18n(
|
|
4673
|
+
setFeatureSuccessor?(featureKey: string, successorKey: string | null): Promise<FeatureCatalogEntryRow>;
|
|
4674
|
+
setQuotaSuccessor?(quotaKey: string, successorKey: string | null): Promise<QuotaCatalogEntryRow>;
|
|
4675
|
+
findFeature(featureKey: string): Promise<FeatureCatalogEntryRow | null>;
|
|
4676
|
+
findQuota(quotaKey: string): Promise<QuotaCatalogEntryRow | null>;
|
|
4677
|
+
setFeatureReview(featureKey: string, data: SetCatalogEntryReviewData): Promise<FeatureCatalogEntryRow>;
|
|
4678
|
+
setQuotaReview(quotaKey: string, data: SetCatalogEntryReviewData): Promise<QuotaCatalogEntryRow>;
|
|
4679
|
+
setFeatureI18n(featureKey: string, i18n: CatalogEntryI18n): Promise<FeatureCatalogEntryRow>;
|
|
4680
|
+
setQuotaI18n(quotaKey: string, i18n: CatalogEntryI18n): Promise<QuotaCatalogEntryRow>;
|
|
3470
4681
|
/** Sets the editable base fields (default locale `label`/`description`). */
|
|
3471
|
-
setFeatureBase(
|
|
3472
|
-
setQuotaBase(
|
|
4682
|
+
setFeatureBase(featureKey: string, data: UpdateCatalogEntryBaseData): Promise<FeatureCatalogEntryRow>;
|
|
4683
|
+
setQuotaBase(quotaKey: string, data: UpdateCatalogEntryBaseData): Promise<QuotaCatalogEntryRow>;
|
|
3473
4684
|
}
|
|
3474
4685
|
/**
|
|
3475
4686
|
* Adapter for `promotions`. **No versioning** — promotions are edited
|
|
@@ -3477,7 +4688,7 @@ interface CatalogEntryRepository {
|
|
|
3477
4688
|
* this against their `promotions` Prisma table.
|
|
3478
4689
|
*/
|
|
3479
4690
|
interface PromotionRepository {
|
|
3480
|
-
list(
|
|
4691
|
+
list(): Promise<PromotionRow[]>;
|
|
3481
4692
|
findById(id: string): Promise<PromotionRow | null>;
|
|
3482
4693
|
create(data: CreatePromotionData): Promise<PromotionRow>;
|
|
3483
4694
|
update(id: string, data: UpdatePromotionData): Promise<PromotionRow>;
|
|
@@ -3485,27 +4696,322 @@ interface PromotionRepository {
|
|
|
3485
4696
|
delete(id: string): Promise<void>;
|
|
3486
4697
|
}
|
|
3487
4698
|
/**
|
|
3488
|
-
* Adapter for `marketing_settings` — one row
|
|
3489
|
-
*
|
|
3490
|
-
*
|
|
4699
|
+
* Adapter for `marketing_settings` — at most one row, which a `CHECK` on the
|
|
4700
|
+
* canonical schema holds rather than convention. `get` returns `null` as long
|
|
4701
|
+
* as the SuperAdmin has saved nothing (then the full `availableLocales` pool
|
|
4702
|
+
* counts as active). `upsert` creates the row or replaces it.
|
|
3491
4703
|
*/
|
|
3492
4704
|
interface MarketingSettingsRepository {
|
|
3493
|
-
get(
|
|
3494
|
-
upsert(
|
|
4705
|
+
get(): Promise<MarketingSettingsRow | null>;
|
|
4706
|
+
upsert(data: UpdateMarketingSettingsData): Promise<MarketingSettingsRow>;
|
|
4707
|
+
}
|
|
4708
|
+
|
|
4709
|
+
/**
|
|
4710
|
+
* Adapter for `checkout_offers`. The offer is an immutable bundle snapshot:
|
|
4711
|
+
* `create` creates it, `update` only allows customization while
|
|
4712
|
+
* `status = 'open'`, `consume` freezes it.
|
|
4713
|
+
*/
|
|
4714
|
+
interface CheckoutOfferRepository {
|
|
4715
|
+
list(filter: CheckoutOfferFilter): Promise<CheckoutOfferRow[]>;
|
|
4716
|
+
findById(id: string): Promise<CheckoutOfferRow | null>;
|
|
4717
|
+
create(data: CreateCheckoutOfferData): Promise<CheckoutOfferRow>;
|
|
4718
|
+
update(id: string, data: UpdateCheckoutOfferData): Promise<CheckoutOfferRow>;
|
|
4719
|
+
/**
|
|
4720
|
+
* Sets `status = 'consumed'` + `consumedAt = NOW()`, and only while the
|
|
4721
|
+
* offer is still `open`: the write carries that condition, so of two
|
|
4722
|
+
* callers consuming at once one succeeds and the other is refused with
|
|
4723
|
+
* `CHECKOUT_OFFER_ALREADY_CONSUMED` (or `CHECKOUT_OFFER_EXPIRED`). A check
|
|
4724
|
+
* before the write cannot decide it, because the status can change in
|
|
4725
|
+
* between.
|
|
4726
|
+
*
|
|
4727
|
+
* With `tx`, the write runs on that transaction, so it is undone with
|
|
4728
|
+
* everything else the transaction wrote. `CheckoutOfferService.conclude`
|
|
4729
|
+
* depends on that; the persistence contract holds both.
|
|
4730
|
+
*/
|
|
4731
|
+
consume(id: string, tx?: TransactionContext): Promise<CheckoutOfferRow>;
|
|
4732
|
+
}
|
|
4733
|
+
|
|
4734
|
+
/** `ACTIVE` is the one in use; `REPLACED` is one a later payment method took over from. */
|
|
4735
|
+
type SubscriberPaymentMethodStatus = 'ACTIVE' | 'REPLACED';
|
|
4736
|
+
interface SubscriberPaymentMethodRecord extends ConfirmedPaymentMethod {
|
|
4737
|
+
id: string;
|
|
4738
|
+
subscriberId: string;
|
|
4739
|
+
/** The account in `config/saas.yaml#payments.accounts` the references belong to. */
|
|
4740
|
+
gatewayAccount: string;
|
|
4741
|
+
/** The provider of that account when the payment method was confirmed. */
|
|
4742
|
+
provider: string;
|
|
4743
|
+
status: SubscriberPaymentMethodStatus;
|
|
4744
|
+
/** When the gateway confirmed the payment method. */
|
|
4745
|
+
confirmedAt: Date;
|
|
4746
|
+
/** When a later payment method took over; `null` while this one is in use. */
|
|
4747
|
+
replacedAt: Date | null;
|
|
4748
|
+
createdAt: Date;
|
|
4749
|
+
}
|
|
4750
|
+
/** What a repository records for a payment method the gateway confirmed. */
|
|
4751
|
+
interface RecordSubscriberPaymentMethodData extends ConfirmedPaymentMethod {
|
|
4752
|
+
subscriberId: string;
|
|
4753
|
+
gatewayAccount: string;
|
|
4754
|
+
provider: string;
|
|
4755
|
+
confirmedAt: Date;
|
|
4756
|
+
}
|
|
4757
|
+
/**
|
|
4758
|
+
* - `activated`: it is the subscriber's payment method now, and the one it
|
|
4759
|
+
* replaced, if any, is `REPLACED`.
|
|
4760
|
+
* - `already-recorded`: the account's reference was recorded before; nothing
|
|
4761
|
+
* was written, and `method` is the row that holds it.
|
|
4762
|
+
* - `superseded`: the subscriber's payment method in use was confirmed after
|
|
4763
|
+
* this one, so this one is recorded as already replaced — confirmations can
|
|
4764
|
+
* arrive in another order than the forms were filled in.
|
|
4765
|
+
*/
|
|
4766
|
+
type RecordSubscriberPaymentMethodOutcome = 'activated' | 'already-recorded' | 'superseded';
|
|
4767
|
+
/**
|
|
4768
|
+
* A change of payment method a tenant started: the gateway session it opened
|
|
4769
|
+
* for the subscriber. A confirmation is recorded only against the setup it
|
|
4770
|
+
* belongs to, so a callback that names another subscriber than the session was
|
|
4771
|
+
* opened for changes nobody's payment method.
|
|
4772
|
+
*/
|
|
4773
|
+
interface SubscriberPaymentMethodSetupData {
|
|
4774
|
+
subscriberId: string;
|
|
4775
|
+
gatewayAccount: string;
|
|
4776
|
+
/** The gateway's session, unique within the account. */
|
|
4777
|
+
sessionRef: string;
|
|
4778
|
+
/** The customer the gateway keeps the payment method under. */
|
|
4779
|
+
customerRef: string;
|
|
4780
|
+
startedAt: Date;
|
|
4781
|
+
}
|
|
4782
|
+
/** Which setup a confirmation claims to complete. */
|
|
4783
|
+
interface SubscriberPaymentMethodSetupMatch {
|
|
4784
|
+
gatewayAccount: string;
|
|
4785
|
+
sessionRef: string;
|
|
4786
|
+
subscriberId: string;
|
|
4787
|
+
}
|
|
4788
|
+
/**
|
|
4789
|
+
* Which payment method a read by reference asks about.
|
|
4790
|
+
*
|
|
4791
|
+
* All three together, because `(gatewayAccount, paymentMethodRef)` is unique
|
|
4792
|
+
* account-wide rather than per subscriber: without the subscriber the question
|
|
4793
|
+
* has no answer that is this caller's.
|
|
4794
|
+
*
|
|
4795
|
+
* One argument rather than three strings in a row, for the reason
|
|
4796
|
+
* `SubscriberPaymentMethodSetupMatch` is one — at a call site three strings of
|
|
4797
|
+
* the same type can be swapped with nothing saying so — and for a second that
|
|
4798
|
+
* is specific to a port. `TransactionContext` is `unknown`, so it accepts a
|
|
4799
|
+
* string: against a positional signature, an implementation whose parameters
|
|
4800
|
+
* are offset by one typechecks and then reads the account out of the
|
|
4801
|
+
* subscriber's place. Against an object it does not.
|
|
4802
|
+
*/
|
|
4803
|
+
interface SubscriberPaymentMethodReference {
|
|
4804
|
+
subscriberId: string;
|
|
4805
|
+
gatewayAccount: string;
|
|
4806
|
+
paymentMethodRef: string;
|
|
4807
|
+
}
|
|
4808
|
+
interface RecordSubscriberPaymentMethodResult {
|
|
4809
|
+
method: SubscriberPaymentMethodRecord;
|
|
4810
|
+
outcome: RecordSubscriberPaymentMethodOutcome;
|
|
4811
|
+
}
|
|
4812
|
+
|
|
4813
|
+
/** A gateway event, as the log records it. */
|
|
4814
|
+
interface PaymentEventClaim {
|
|
4815
|
+
/** The account the event came from; an event identifier is unique only within it. */
|
|
4816
|
+
gatewayAccount: string;
|
|
4817
|
+
eventId: string;
|
|
4818
|
+
provider: string;
|
|
4819
|
+
/** The gateway session the event is about, where it is about one. */
|
|
4820
|
+
sessionId: string | null;
|
|
4821
|
+
kind: PaymentGatewayEvent['kind'];
|
|
4822
|
+
/** What the event said, without anything that names or reaches a person. */
|
|
4823
|
+
summary: Record<string, unknown>;
|
|
4824
|
+
}
|
|
4825
|
+
/**
|
|
4826
|
+
* The events a gateway sent, each handled once.
|
|
4827
|
+
*
|
|
4828
|
+
* Gateways deliver at least once, so the same event arrives again after a
|
|
4829
|
+
* timeout or a retry. An event is claimed on the transaction that writes what
|
|
4830
|
+
* it changes: when that transaction rolls back, the claim goes with it and the
|
|
4831
|
+
* gateway's retry is handled rather than discarded as a duplicate.
|
|
4832
|
+
*/
|
|
4833
|
+
interface PaymentEventLog {
|
|
4834
|
+
/**
|
|
4835
|
+
* Records the event and returns `true`, or returns `false` when this account
|
|
4836
|
+
* already recorded it, writing nothing.
|
|
4837
|
+
*
|
|
4838
|
+
* The duplicate must not raise: on PostgreSQL an error aborts the
|
|
4839
|
+
* transaction it happened on, and the caller's transaction has to stay
|
|
4840
|
+
* usable. An `INSERT … ON CONFLICT DO NOTHING` does both — and when another
|
|
4841
|
+
* transaction holds the same claim uncommitted, it waits for that one and
|
|
4842
|
+
* answers by its outcome.
|
|
4843
|
+
*
|
|
4844
|
+
* A confirmation of a session this account already confirmed is a duplicate
|
|
4845
|
+
* as well, whatever its `eventId`: one session is set up once, and a gateway
|
|
4846
|
+
* may report it through more than one event. `sql/constraints.postgres.sql`
|
|
4847
|
+
* holds that as a second unique index, and the untargeted `DO NOTHING`
|
|
4848
|
+
* answers for it too.
|
|
4849
|
+
*/
|
|
4850
|
+
claim(claim: PaymentEventClaim, tx: TransactionContext): Promise<boolean>;
|
|
4851
|
+
/**
|
|
4852
|
+
* Takes the session off a claim whose event changed nothing, so the next
|
|
4853
|
+
* event about that session is handled instead of taken for the duplicate it
|
|
4854
|
+
* is not. The claim itself stays: that one event is never handled twice.
|
|
4855
|
+
*
|
|
4856
|
+
* The session is held from the claim onwards, which is what keeps two
|
|
4857
|
+
* confirmations delivered at once from both taking effect; an event that
|
|
4858
|
+
* turned out to have nothing to do gives it back on the same transaction.
|
|
4859
|
+
*/
|
|
4860
|
+
releaseSession(gatewayAccount: string, eventId: string, tx: TransactionContext): Promise<void>;
|
|
4861
|
+
}
|
|
4862
|
+
/**
|
|
4863
|
+
* The payment methods of subscribers, as references at the gateway accounts
|
|
4864
|
+
* that confirmed them.
|
|
4865
|
+
*
|
|
4866
|
+
* A subscriber has at most one `ACTIVE` payment method; the database holds
|
|
4867
|
+
* that, so two confirmations recorded at once for one subscriber end with one.
|
|
4868
|
+
*
|
|
4869
|
+
* **Within an account, a reference belongs to exactly one subscriber**, and
|
|
4870
|
+
* keeps belonging to it once a newer payment method has replaced it. Every
|
|
4871
|
+
* method that reaches a payment method therefore names the subscriber it is
|
|
4872
|
+
* about, and an implementation answers about that one only. `accountsInUse` is
|
|
4873
|
+
* the exception and says so at itself: it counts accounts rather than handing
|
|
4874
|
+
* out a row.
|
|
4875
|
+
*
|
|
4876
|
+
* That matters on a gateway callback, which is where these are reached: it
|
|
4877
|
+
* arrives without a session, so an installation that keeps its tenants apart
|
|
4878
|
+
* with a policy lifts that policy for it, and what the question names is then
|
|
4879
|
+
* all that bounds it.
|
|
4880
|
+
*/
|
|
4881
|
+
interface SubscriberPaymentMethodRepository {
|
|
4882
|
+
/**
|
|
4883
|
+
* Records a confirmed payment method for its subscriber, under a lock on the
|
|
4884
|
+
* subscriber so two confirmations take turns. See
|
|
4885
|
+
* `RecordSubscriberPaymentMethodOutcome` for the three outcomes.
|
|
4886
|
+
*
|
|
4887
|
+
* A confirmation carrying a reference **another** subscriber holds is a
|
|
4888
|
+
* fourth case and has no outcome: it is refused with
|
|
4889
|
+
* `ForeignPaymentMethodReferenceError`, because the account's reference is
|
|
4890
|
+
* that subscriber's and answering `already-recorded` would hand its payment
|
|
4891
|
+
* method to a caller acting for somebody else.
|
|
4892
|
+
*
|
|
4893
|
+
* Two subscribers can reach that reference at the same time: the lock is on
|
|
4894
|
+
* the subscriber, so confirmations for two of them do not take turns, and a
|
|
4895
|
+
* read sees nothing of a row the other has not committed. Reading is
|
|
4896
|
+
* therefore not enough to decide it. **Claim the reference with the first
|
|
4897
|
+
* write, conflict-free** — `ON CONFLICT DO NOTHING` on its unique key, or
|
|
4898
|
+
* whatever the store spells that as — and refuse when the claim takes no
|
|
4899
|
+
* row. Both shipped adapters do exactly that, and it is what lets the
|
|
4900
|
+
* refusal say the same thing everywhere: the key decides, not an error
|
|
4901
|
+
* whose shape belongs to one driver.
|
|
4902
|
+
*
|
|
4903
|
+
* The order is part of the contract, not an implementation detail. Because
|
|
4904
|
+
* the claim is the first write, a refusal leaves the caller's transaction
|
|
4905
|
+
* as it found it — nothing to undo, and nothing that makes it unusable.
|
|
4906
|
+
* An implementation that writes before it claims cannot promise that.
|
|
4907
|
+
*/
|
|
4908
|
+
recordConfirmed(data: RecordSubscriberPaymentMethodData, tx?: TransactionContext): Promise<RecordSubscriberPaymentMethodResult>;
|
|
4909
|
+
/** The subscriber's payment method in use, or `null` when it has none. */
|
|
4910
|
+
findActive(subscriberId: string, tx?: TransactionContext): Promise<SubscriberPaymentMethodRecord | null>;
|
|
4911
|
+
/**
|
|
4912
|
+
* The payment method the reference names, whatever its status — and `null`
|
|
4913
|
+
* when that subscriber holds none under it, which includes the case of
|
|
4914
|
+
* another subscriber holding the reference.
|
|
4915
|
+
*
|
|
4916
|
+
* The one read that reaches a payment method a newer one replaced, which is
|
|
4917
|
+
* how `@saasicat/persistence-testing` verifies that an implementation keeps
|
|
4918
|
+
* the history rather than overwriting the row (`SC-PRIC-030`).
|
|
4919
|
+
*
|
|
4920
|
+
* The subscriber is what bounds the read. Without it an implementation
|
|
4921
|
+
* would have only the account's reference to go on, which is unique
|
|
4922
|
+
* account-wide: inside a tenant's context a policy would answer `null` and
|
|
4923
|
+
* on a gateway callback, where that policy is lifted, the same call would
|
|
4924
|
+
* answer with somebody else's row. One question, two answers by where it
|
|
4925
|
+
* was asked, is what naming the subscriber removes.
|
|
4926
|
+
*/
|
|
4927
|
+
findByReference(reference: SubscriberPaymentMethodReference, tx?: TransactionContext): Promise<SubscriberPaymentMethodRecord | null>;
|
|
4928
|
+
/**
|
|
4929
|
+
* Every account that holds a payment method in use, each once.
|
|
4930
|
+
*
|
|
4931
|
+
* Platform-wide: anchored by no subscriber and no tenant, and a start makes
|
|
4932
|
+
* it before anything is served, to refuse a configuration that no longer
|
|
4933
|
+
* names an account somebody's payment method is held at. An implementation
|
|
4934
|
+
* on a tenant-scoped client must read RLS-exempt — the platform wraps the
|
|
4935
|
+
* call in `RlsBypassPort`, and one that answers with the caller's tenant
|
|
4936
|
+
* scope instead returns an empty list at a boot, where there is no tenant,
|
|
4937
|
+
* so the check that exists to refuse passes. The persistence contract runs
|
|
4938
|
+
* with no policy forced, so it cannot catch that for you.
|
|
4939
|
+
*/
|
|
4940
|
+
accountsInUse(): Promise<string[]>;
|
|
4941
|
+
/**
|
|
4942
|
+
* Records a change of payment method a tenant started, open until its
|
|
4943
|
+
* confirmation completes it.
|
|
4944
|
+
*
|
|
4945
|
+
* One session is one setup: the account and the session are unique together,
|
|
4946
|
+
* and a second setup for a session already recorded raises. A gateway hands
|
|
4947
|
+
* out a session per start, so an adapter that returns one twice is the
|
|
4948
|
+
* defect this refuses to write over.
|
|
4949
|
+
*/
|
|
4950
|
+
recordSetup(data: SubscriberPaymentMethodSetupData, tx?: TransactionContext): Promise<void>;
|
|
4951
|
+
/**
|
|
4952
|
+
* Completes the open setup the account, the session and the subscriber all
|
|
4953
|
+
* name, and returns `true` — or returns `false`, writing nothing, when no
|
|
4954
|
+
* open setup matches all three: none was started, it was started for another
|
|
4955
|
+
* subscriber, or it is complete already. A single conditional write, so two
|
|
4956
|
+
* confirmations for one setup complete it once.
|
|
4957
|
+
*/
|
|
4958
|
+
completeSetup(match: SubscriberPaymentMethodSetupMatch, completedAt: Date, tx?: TransactionContext): Promise<boolean>;
|
|
3495
4959
|
}
|
|
3496
4960
|
|
|
4961
|
+
/** Which changes to list. */
|
|
4962
|
+
interface SettingsChangeFilter {
|
|
4963
|
+
/** Only changes an operator has, or has not, acknowledged. Omitted: both. */
|
|
4964
|
+
acknowledged?: boolean;
|
|
4965
|
+
/** The most recently recorded ones. Omitted: every matching change. */
|
|
4966
|
+
limit?: number;
|
|
4967
|
+
}
|
|
3497
4968
|
/**
|
|
3498
|
-
*
|
|
3499
|
-
*
|
|
3500
|
-
* `
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
|
|
4969
|
+
* Stores what the installation applied and what changed between two boots.
|
|
4970
|
+
*
|
|
4971
|
+
* `applied_settings` holds one row for the installation; `settings_changes`
|
|
4972
|
+
* holds one row per boot that found the fingerprint moved. An adapter
|
|
4973
|
+
* translates: it does not decide what a change is, and it does not read the
|
|
4974
|
+
* row back into anything that runs.
|
|
4975
|
+
*
|
|
4976
|
+
* Both writes are guarded on the fingerprint the caller read. Several replicas
|
|
4977
|
+
* of one installation start together after one edit of the file, each reads
|
|
4978
|
+
* the same record and each finds the same difference; the guard is what makes
|
|
4979
|
+
* one of them the boot that recorded it and the others boots that found it
|
|
4980
|
+
* recorded. Without it every replica would write the change and mail the
|
|
4981
|
+
* addresses, once per replica.
|
|
4982
|
+
*/
|
|
4983
|
+
interface AppliedSettingsPort {
|
|
4984
|
+
/** The record, or null before the first boot that could write one. */
|
|
4985
|
+
readApplied(): Promise<AppliedSettingsRecord | null>;
|
|
4986
|
+
/**
|
|
4987
|
+
* Replaces the installation's record — there is only ever the one row —
|
|
4988
|
+
* provided the stored record still carries `expectedFingerprint`: the
|
|
4989
|
+
* fingerprint the caller read, or `null` where it read no record. Returns
|
|
4990
|
+
* whether it did. `false` means the record moved between the caller's read
|
|
4991
|
+
* and this write, and nothing was written: another boot got there first.
|
|
4992
|
+
*/
|
|
4993
|
+
writeApplied(record: AppliedSettingsRecord, expectedFingerprint: string | null): Promise<boolean>;
|
|
4994
|
+
/**
|
|
4995
|
+
* Appends a change a boot noticed and replaces the record it supersedes, in
|
|
4996
|
+
* one step: both land, or neither does. Guarded like `writeApplied`, on the
|
|
4997
|
+
* fingerprint of the record the change was noticed against. Returns the
|
|
4998
|
+
* change as stored — the id is the adapter's to assign — or `null` where
|
|
4999
|
+
* the record had already moved on: another boot noticed first, and the
|
|
5000
|
+
* change is that boot's to report.
|
|
5001
|
+
*/
|
|
5002
|
+
recordChange(change: NewSettingsChange, record: AppliedSettingsRecord, expectedFingerprint: string): Promise<SettingsChangeRecord | null>;
|
|
5003
|
+
/**
|
|
5004
|
+
* Changes, the most recently recorded first: the order the record went
|
|
5005
|
+
* through them, which the database numbers at each write — not the order
|
|
5006
|
+
* of the moments they carry, which are the recording starts' clocks.
|
|
5007
|
+
*/
|
|
5008
|
+
listChanges(filter?: SettingsChangeFilter): Promise<SettingsChangeRecord[]>;
|
|
5009
|
+
/**
|
|
5010
|
+
* Marks a change as seen. Returns the updated record, or null where no
|
|
5011
|
+
* change has that id. A change already acknowledged keeps its first
|
|
5012
|
+
* acknowledgement — repeating the action changes nothing.
|
|
5013
|
+
*/
|
|
5014
|
+
acknowledgeChange(id: string, acknowledgedBy: string, acknowledgedAt: Date): Promise<SettingsChangeRecord | null>;
|
|
3509
5015
|
}
|
|
3510
5016
|
|
|
3511
5017
|
/** Class reference usable as a DI token (e.g. the consumer's `PrismaService`). */
|
|
@@ -3565,12 +5071,20 @@ interface SaaSiCatPersistenceCore {
|
|
|
3565
5071
|
auditQuery?: PersistenceProvider<AuditQueryPort>;
|
|
3566
5072
|
/** Aggregation for the admin stats dashboard. */
|
|
3567
5073
|
auditStats?: PersistenceProvider<AuditStatsPort>;
|
|
5074
|
+
/**
|
|
5075
|
+
* The record of the applied configuration (`SettingsModule`). Optional so
|
|
5076
|
+
* an adapter written before it existed keeps working; without it the
|
|
5077
|
+
* platform says once at boot that it is not recording.
|
|
5078
|
+
*/
|
|
5079
|
+
appliedSettings?: PersistenceProvider<AppliedSettingsPort>;
|
|
3568
5080
|
}
|
|
3569
5081
|
/** Repositories for the entitlement/contract loop (`EntitlementModule`). */
|
|
3570
5082
|
interface SaaSiCatPersistenceEntitlement {
|
|
3571
5083
|
subscriptionRepository: PersistenceProvider<SubscriptionRepository>;
|
|
3572
5084
|
planVersionRepository: PersistenceProvider<PlanVersionRepository>;
|
|
3573
5085
|
subscriptionContractRepository?: PersistenceProvider<SubscriptionContractRepository>;
|
|
5086
|
+
/** The parties contracts are concluded with; required wherever contracts are written. */
|
|
5087
|
+
subscriberRepository?: PersistenceProvider<SubscriberRepository>;
|
|
3574
5088
|
subscriptionBundleRepository?: PersistenceProvider<SubscriptionBundleRepository>;
|
|
3575
5089
|
bundleRepository?: PersistenceProvider<BundleRepository>;
|
|
3576
5090
|
}
|
|
@@ -3597,6 +5111,15 @@ interface SaaSiCatPersistenceTenantBilling {
|
|
|
3597
5111
|
subscriptionWritePort: PersistenceProvider<TenantSubscriptionWritePort>;
|
|
3598
5112
|
usageSnapshotPort?: PersistenceProvider<UsageSnapshotPort>;
|
|
3599
5113
|
}
|
|
5114
|
+
/**
|
|
5115
|
+
* The record of payment gateway callbacks and the payment methods they confirm
|
|
5116
|
+
* (`payments` in `SaaSiCatModule.forRoot`). Both are written on the transaction
|
|
5117
|
+
* a callback is handled on, so they come from the adapter that runs it.
|
|
5118
|
+
*/
|
|
5119
|
+
interface SaaSiCatPersistencePayments {
|
|
5120
|
+
paymentEventLog: PersistenceProvider<PaymentEventLog>;
|
|
5121
|
+
subscriberPaymentMethodRepository: PersistenceProvider<SubscriberPaymentMethodRepository>;
|
|
5122
|
+
}
|
|
3600
5123
|
/** Read/write backing for the standard SuperAdmin resource pages. */
|
|
3601
5124
|
interface SaaSiCatPersistenceAdminResources {
|
|
3602
5125
|
resources: PersistenceProvider<AdminResourcesPort>;
|
|
@@ -3613,6 +5136,8 @@ interface SaaSiCatPersistencePromo {
|
|
|
3613
5136
|
validationLogRepository: PersistenceProvider<PromoCodeValidationLogRepository>;
|
|
3614
5137
|
subscriptionLookup: PersistenceProvider<PromoSubscriptionLookup>;
|
|
3615
5138
|
revenueAggregator: PersistenceProvider<PromoRevenueDeductionAggregator>;
|
|
5139
|
+
/** The slots a code keeps for checkouts; left out, no checkout holds one. */
|
|
5140
|
+
holdRepository?: PersistenceProvider<PromoCodeHoldRepository>;
|
|
3616
5141
|
}
|
|
3617
5142
|
/**
|
|
3618
5143
|
* Aggregate persistence bundle. Produced by adapter factories such as
|
|
@@ -3631,6 +5156,8 @@ interface SaaSiCatPersistenceAdapter {
|
|
|
3631
5156
|
/** Tenant, user, audit and subscription resources for the SuperAdmin UI. */
|
|
3632
5157
|
adminResources?: SaaSiCatPersistenceAdminResources;
|
|
3633
5158
|
promo?: SaaSiCatPersistencePromo;
|
|
5159
|
+
/** Payment gateway callbacks and subscriber payment methods. */
|
|
5160
|
+
payments?: SaaSiCatPersistencePayments;
|
|
3634
5161
|
/** DB hydration of the plan catalog at boot (`PlanCatalogModule`). */
|
|
3635
5162
|
planCatalogReadSink?: PersistenceProvider<PlanCatalogReadSink>;
|
|
3636
5163
|
/** One-shot `saas.yaml → DB` import. */
|
|
@@ -3703,6 +5230,8 @@ declare const CATALOG_ERROR_CODES: {
|
|
|
3703
5230
|
readonly BUNDLE_VERSION_SUPERSEDED: "BUNDLE_VERSION_SUPERSEDED";
|
|
3704
5231
|
readonly BUNDLE_VERSION_REGRESSION: "BUNDLE_VERSION_REGRESSION";
|
|
3705
5232
|
readonly BUNDLE_VERSION_ZERO_PRICE: "BUNDLE_VERSION_ZERO_PRICE";
|
|
5233
|
+
readonly BUNDLE_VERSION_NO_PRICE: "BUNDLE_VERSION_NO_PRICE";
|
|
5234
|
+
readonly BUNDLE_VERSION_NOT_PRICED_FOR_PLAN: "BUNDLE_VERSION_NOT_PRICED_FOR_PLAN";
|
|
3706
5235
|
readonly BUNDLE_VERSION_DISCARD_NOT_IMPLEMENTED: "BUNDLE_VERSION_DISCARD_NOT_IMPLEMENTED";
|
|
3707
5236
|
readonly BUNDLE_VERSION_VALID_FROM_REQUIRED: "BUNDLE_VERSION_VALID_FROM_REQUIRED";
|
|
3708
5237
|
readonly BUNDLE_VERSION_VALID_FROM_INVALID: "BUNDLE_VERSION_VALID_FROM_INVALID";
|
|
@@ -3720,6 +5249,12 @@ declare const CATALOG_ERROR_CODES: {
|
|
|
3720
5249
|
readonly FEATURE_NOT_FOUND: "FEATURE_NOT_FOUND";
|
|
3721
5250
|
readonly QUOTA_NOT_FOUND: "QUOTA_NOT_FOUND";
|
|
3722
5251
|
readonly PROMOTION_NOT_FOUND: "PROMOTION_NOT_FOUND";
|
|
5252
|
+
/**
|
|
5253
|
+
* A promotion's value is not one its type takes: a percentage above 0 and
|
|
5254
|
+
* at most 100, an amount above 0, an intro price of at least 0 for a whole
|
|
5255
|
+
* number of months, or a whole number of free months.
|
|
5256
|
+
*/
|
|
5257
|
+
readonly PROMOTION_VALUE_INVALID: "PROMOTION_VALUE_INVALID";
|
|
3723
5258
|
readonly MARKETING_PROJECTION_NOT_FOUND: "MARKETING_PROJECTION_NOT_FOUND";
|
|
3724
5259
|
readonly PLAN_ALREADY_EXISTS: "PLAN_ALREADY_EXISTS";
|
|
3725
5260
|
readonly BUNDLE_ALREADY_EXISTS: "BUNDLE_ALREADY_EXISTS";
|
|
@@ -3730,6 +5265,10 @@ declare const CATALOG_ERROR_CODES: {
|
|
|
3730
5265
|
readonly QUOTA_NOT_IN_DISCOVERY_SNAPSHOT: "QUOTA_NOT_IN_DISCOVERY_SNAPSHOT";
|
|
3731
5266
|
readonly DISCOVERY_STATUS_TRANSITION_INVALID: "DISCOVERY_STATUS_TRANSITION_INVALID";
|
|
3732
5267
|
readonly DISCOVERY_NOT_INITIALIZED: "DISCOVERY_NOT_INITIALIZED";
|
|
5268
|
+
/** The uploaded document is not a plan catalog — unparseable, or not an object. */
|
|
5269
|
+
readonly PLAN_CATALOG_UNREADABLE: "PLAN_CATALOG_UNREADABLE";
|
|
5270
|
+
/** It parsed, and then failed the schema or a cross-field rule. */
|
|
5271
|
+
readonly PLAN_CATALOG_INVALID: "PLAN_CATALOG_INVALID";
|
|
3733
5272
|
};
|
|
3734
5273
|
type CatalogErrorCode = (typeof CATALOG_ERROR_CODES)[keyof typeof CATALOG_ERROR_CODES];
|
|
3735
5274
|
/** Bundle bookings on a tenant subscription. */
|
|
@@ -3743,6 +5282,8 @@ declare const BILLING_ERROR_CODES: {
|
|
|
3743
5282
|
readonly BUNDLE_ALREADY_SUBSCRIBED: "BUNDLE_ALREADY_SUBSCRIBED";
|
|
3744
5283
|
readonly BUNDLE_INCOMPATIBLE_WITH_PLAN: "BUNDLE_INCOMPATIBLE_WITH_PLAN";
|
|
3745
5284
|
readonly BUNDLE_NOT_SELF_SERVICE: "BUNDLE_NOT_SELF_SERVICE";
|
|
5285
|
+
readonly BUNDLE_CYCLE_EXCEEDS_PLAN: "BUNDLE_CYCLE_EXCEEDS_PLAN";
|
|
5286
|
+
readonly BUNDLE_NOT_PRICED_FOR_THIS_PLAN: "BUNDLE_NOT_PRICED_FOR_THIS_PLAN";
|
|
3746
5287
|
readonly SUBSCRIPTION_BUNDLE_ALREADY_CANCELLED: "SUBSCRIPTION_BUNDLE_ALREADY_CANCELLED";
|
|
3747
5288
|
readonly SUBSCRIPTION_BUNDLE_NOT_CANCELLED: "SUBSCRIPTION_BUNDLE_NOT_CANCELLED";
|
|
3748
5289
|
readonly SUBSCRIPTION_BUNDLE_CANCELLATION_EFFECTIVE: "SUBSCRIPTION_BUNDLE_CANCELLATION_EFFECTIVE";
|
|
@@ -3756,8 +5297,79 @@ declare const BILLING_ERROR_CODES: {
|
|
|
3756
5297
|
readonly PLAN_NOT_IN_CATALOG: "PLAN_NOT_IN_CATALOG";
|
|
3757
5298
|
/** Plan exists but cannot be booked via self-service. */
|
|
3758
5299
|
readonly PLAN_NOT_SELF_SERVICE: "PLAN_NOT_SELF_SERVICE";
|
|
5300
|
+
/**
|
|
5301
|
+
* Plan carries no price for the requested billing cycle, so it is not sold
|
|
5302
|
+
* in it: a plan without a yearly price is a monthly plan.
|
|
5303
|
+
*/
|
|
5304
|
+
readonly PLAN_NOT_SOLD_IN_CYCLE: "PLAN_NOT_SOLD_IN_CYCLE";
|
|
3759
5305
|
/** Plan change refused. Carries `blockers[]` with their own codes. */
|
|
3760
5306
|
readonly PLAN_CHANGE_BLOCKED: "PLAN_CHANGE_BLOCKED";
|
|
5307
|
+
/**
|
|
5308
|
+
* The subscription moved between the read a request was decided on and the
|
|
5309
|
+
* write it attempted, so nothing was written. The caller reloads and asks
|
|
5310
|
+
* again.
|
|
5311
|
+
*/
|
|
5312
|
+
readonly SUBSCRIPTION_CHANGED: "SUBSCRIPTION_CHANGED";
|
|
5313
|
+
/**
|
|
5314
|
+
* The tenant has no subscription to act on.
|
|
5315
|
+
*
|
|
5316
|
+
* `SUBSCRIPTION_NOT_FOUND` states the same fact on the read routes. Both
|
|
5317
|
+
* are already on the wire and a code is renamed only deliberately, so both
|
|
5318
|
+
* are named here rather than one being dropped behind a consumer's back.
|
|
5319
|
+
*/
|
|
5320
|
+
readonly NO_SUBSCRIPTION: "NO_SUBSCRIPTION";
|
|
5321
|
+
/**
|
|
5322
|
+
* The cancellation date the reader was shown is no longer the one the rules
|
|
5323
|
+
* return, so the confirmation is refused rather than silently applied.
|
|
5324
|
+
* Carries the recomputed dates, so the page can re-ask instead of guessing.
|
|
5325
|
+
*/
|
|
5326
|
+
readonly CANCELLATION_TERMS_CHANGED: "CANCELLATION_TERMS_CHANGED";
|
|
5327
|
+
/** The subscription has ended; its plan can no longer be changed. */
|
|
5328
|
+
readonly SUBSCRIPTION_ENDED: "SUBSCRIPTION_ENDED";
|
|
5329
|
+
/** An active special contract blocks self-service plan changes. */
|
|
5330
|
+
readonly PLAN_LOCKED: "PLAN_LOCKED";
|
|
5331
|
+
/** Current usage of one quota exceeds what the target plan allows. */
|
|
5332
|
+
readonly QUOTA_OVER_TARGET: "QUOTA_OVER_TARGET";
|
|
5333
|
+
/** The change drops features the tenant has today. */
|
|
5334
|
+
readonly FEATURE_LOST: "FEATURE_LOST";
|
|
5335
|
+
readonly FEATURES_LOST: "FEATURES_LOST";
|
|
5336
|
+
/** Target plan and cycle already match what is in place. */
|
|
5337
|
+
readonly NO_CHANGE: "NO_CHANGE";
|
|
5338
|
+
/** A shorter cycle cannot start inside the term already running. */
|
|
5339
|
+
readonly CYCLE_SHORTENS_AT_TERM_END: "CYCLE_SHORTENS_AT_TERM_END";
|
|
5340
|
+
/** A cancelled subscription cannot change its billing cycle. */
|
|
5341
|
+
readonly CANCELLATION_LOCKS_THE_CYCLE: "CANCELLATION_LOCKS_THE_CYCLE";
|
|
5342
|
+
/**
|
|
5343
|
+
* A bundle the tenant already holds runs past the cycle they are moving to.
|
|
5344
|
+
*
|
|
5345
|
+
* Its own code rather than `BUNDLE_CYCLE_EXCEEDS_PLAN`, which states the
|
|
5346
|
+
* same rule about a booking that has not been made yet. The two need
|
|
5347
|
+
* different sentences: this one can name the day the obstacle lifts and
|
|
5348
|
+
* tell the reader to cancel the booking, and that advice is wrong for
|
|
5349
|
+
* someone who is only about to book. One template cannot serve both.
|
|
5350
|
+
*/
|
|
5351
|
+
readonly BUNDLE_BOOKING_OUTLASTS_TARGET_CYCLE: "BUNDLE_BOOKING_OUTLASTS_TARGET_CYCLE";
|
|
5352
|
+
/**
|
|
5353
|
+
* Features of the previewed bundle are already covered by the plan or by
|
|
5354
|
+
* another booked bundle. A warning rather than a blocker: paying twice is
|
|
5355
|
+
* the customer's decision, and the preview only has to say so first.
|
|
5356
|
+
*/
|
|
5357
|
+
readonly REDUNDANT_FEATURES: "REDUNDANT_FEATURES";
|
|
5358
|
+
/**
|
|
5359
|
+
* A booking's minimum term outlasts the period being cancelled, so the
|
|
5360
|
+
* cancellation takes effect at the end of the term, not of the period.
|
|
5361
|
+
*/
|
|
5362
|
+
readonly MINIMUM_TERM_BINDS: "MINIMUM_TERM_BINDS";
|
|
5363
|
+
/**
|
|
5364
|
+
* The previewed bundle requires features that neither the plan nor an
|
|
5365
|
+
* active booking provides.
|
|
5366
|
+
*
|
|
5367
|
+
* The same string is a `StrictModeWarningCode` in `bundle.types.ts`, where
|
|
5368
|
+
* it names the catalogue-authoring reading of the rule and travels with its
|
|
5369
|
+
* own message. This declaration is the booking preview's blocker, which a
|
|
5370
|
+
* tenant reads and therefore needs a shipped text for.
|
|
5371
|
+
*/
|
|
5372
|
+
readonly BUNDLE_FEATURE_DEPENDENCY_UNSATISFIED: "BUNDLE_FEATURE_DEPENDENCY_UNSATISFIED";
|
|
3761
5373
|
readonly NO_PENDING_PLAN_VERSION: "NO_PENDING_PLAN_VERSION";
|
|
3762
5374
|
readonly ONBOARDING_CREATE_FAILED: "ONBOARDING_CREATE_FAILED";
|
|
3763
5375
|
readonly BUNDLE_PREVIEW_ARGUMENT_AMBIGUOUS: "BUNDLE_PREVIEW_ARGUMENT_AMBIGUOUS";
|
|
@@ -3778,20 +5390,66 @@ declare const CONTRACT_ERROR_CODES: {
|
|
|
3778
5390
|
readonly CHECKOUT_OFFER_BUNDLE_LINE_ITEMS_REQUIRED: "CHECKOUT_OFFER_BUNDLE_LINE_ITEMS_REQUIRED";
|
|
3779
5391
|
readonly CHECKOUT_OFFER_BUNDLE_VERSION_NOT_BOOKABLE: "CHECKOUT_OFFER_BUNDLE_VERSION_NOT_BOOKABLE";
|
|
3780
5392
|
readonly CHECKOUT_OFFER_FEATURE_DEPENDENCY_UNSATISFIED: "CHECKOUT_OFFER_FEATURE_DEPENDENCY_UNSATISFIED";
|
|
5393
|
+
readonly CHECKOUT_OFFER_PLAN_NOT_OFFERED: "CHECKOUT_OFFER_PLAN_NOT_OFFERED";
|
|
5394
|
+
readonly CHECKOUT_OFFER_BUNDLE_NOT_OFFERED: "CHECKOUT_OFFER_BUNDLE_NOT_OFFERED";
|
|
5395
|
+
readonly CHECKOUT_OFFER_PROMO_CODE_NOT_ACCEPTED: "CHECKOUT_OFFER_PROMO_CODE_NOT_ACCEPTED";
|
|
5396
|
+
readonly CHECKOUT_OFFER_PRICE_NOT_CURRENT: "CHECKOUT_OFFER_PRICE_NOT_CURRENT";
|
|
3781
5397
|
readonly SUBSCRIPTION_CONTRACT_LINE_ITEMS_REQUIRED: "SUBSCRIPTION_CONTRACT_LINE_ITEMS_REQUIRED";
|
|
3782
5398
|
readonly SUBSCRIPTION_CONTRACT_PLAN_LINE_ITEM_REQUIRED: "SUBSCRIPTION_CONTRACT_PLAN_LINE_ITEM_REQUIRED";
|
|
3783
5399
|
readonly SUBSCRIPTION_CONTRACT_INVALID_DATE: "SUBSCRIPTION_CONTRACT_INVALID_DATE";
|
|
3784
5400
|
readonly SUBSCRIPTION_CONTRACT_INVALID_WINDOW: "SUBSCRIPTION_CONTRACT_INVALID_WINDOW";
|
|
5401
|
+
readonly SUBSCRIPTION_CONTRACT_LINE_ITEM_TAX_MISMATCH: "SUBSCRIPTION_CONTRACT_LINE_ITEM_TAX_MISMATCH";
|
|
5402
|
+
readonly SUBSCRIPTION_CONTRACT_TAX_RATE_NOT_PERCENT: "SUBSCRIPTION_CONTRACT_TAX_RATE_NOT_PERCENT";
|
|
5403
|
+
readonly SUBSCRIPTION_CONTRACT_LINE_ITEM_CURRENCY_MISMATCH: "SUBSCRIPTION_CONTRACT_LINE_ITEM_CURRENCY_MISMATCH";
|
|
5404
|
+
/**
|
|
5405
|
+
* The lines do not add up to a total the contract states, counted as often
|
|
5406
|
+
* as each falls due in one period. `field` names the total.
|
|
5407
|
+
*/
|
|
5408
|
+
readonly SUBSCRIPTION_CONTRACT_LINES_DO_NOT_ADD_UP: "SUBSCRIPTION_CONTRACT_LINES_DO_NOT_ADD_UP";
|
|
5409
|
+
/** Something that takes money off states a negative amount. */
|
|
5410
|
+
readonly SUBSCRIPTION_CONTRACT_DISCOUNT_NEGATIVE: "SUBSCRIPTION_CONTRACT_DISCOUNT_NEGATIVE";
|
|
3785
5411
|
readonly SUBSCRIPTION_CONTRACT_TERMINATION_BEFORE_START: "SUBSCRIPTION_CONTRACT_TERMINATION_BEFORE_START";
|
|
3786
5412
|
readonly CHECKOUT_OFFER_NOT_FOUND: "CHECKOUT_OFFER_NOT_FOUND";
|
|
3787
5413
|
readonly CHECKOUT_OFFER_EXPIRED: "CHECKOUT_OFFER_EXPIRED";
|
|
3788
5414
|
readonly CHECKOUT_OFFER_ALREADY_CONSUMED: "CHECKOUT_OFFER_ALREADY_CONSUMED";
|
|
3789
5415
|
readonly CHECKOUT_OFFER_NOT_CONSUMED: "CHECKOUT_OFFER_NOT_CONSUMED";
|
|
5416
|
+
/**
|
|
5417
|
+
* The offer changed between the checks of `conclude` and the transaction
|
|
5418
|
+
* that consumed it, so the contract checked is not the one the offer now
|
|
5419
|
+
* describes. Nothing was written; load the offer and conclude it again.
|
|
5420
|
+
*/
|
|
5421
|
+
readonly CHECKOUT_OFFER_CHANGED: "CHECKOUT_OFFER_CHANGED";
|
|
3790
5422
|
readonly SUBSCRIPTION_CONTRACT_NOT_FOUND: "SUBSCRIPTION_CONTRACT_NOT_FOUND";
|
|
3791
5423
|
readonly NO_ACTIVE_SUBSCRIPTION_CONTRACT: "NO_ACTIVE_SUBSCRIPTION_CONTRACT";
|
|
3792
5424
|
readonly SUBSCRIPTION_CONTRACT_ALREADY_CLOSED: "SUBSCRIPTION_CONTRACT_ALREADY_CLOSED";
|
|
3793
5425
|
};
|
|
3794
5426
|
type ContractErrorCode = (typeof CONTRACT_ERROR_CODES)[keyof typeof CONTRACT_ERROR_CODES];
|
|
5427
|
+
/** The parties contracts are concluded with (`SubscriberService`), and their absence. */
|
|
5428
|
+
declare const SUBSCRIBER_ERROR_CODES: {
|
|
5429
|
+
/**
|
|
5430
|
+
* A contract, a plan change or a booking for a tenant that has no
|
|
5431
|
+
* subscriber. Nothing is agreed or charged without the party to it.
|
|
5432
|
+
* Carries `tenantId`.
|
|
5433
|
+
*/
|
|
5434
|
+
readonly SUBSCRIBER_REQUIRED: "SUBSCRIBER_REQUIRED";
|
|
5435
|
+
/** The tenant already has a live subscriber. Carries `tenantId`. */
|
|
5436
|
+
readonly SUBSCRIBER_ALREADY_EXISTS: "SUBSCRIBER_ALREADY_EXISTS";
|
|
5437
|
+
readonly SUBSCRIBER_NOT_FOUND: "SUBSCRIBER_NOT_FOUND";
|
|
5438
|
+
readonly SUBSCRIBER_LEGAL_NAME_REQUIRED: "SUBSCRIBER_LEGAL_NAME_REQUIRED";
|
|
5439
|
+
/**
|
|
5440
|
+
* A detail that has a form — the country, the invoice email — is not in it,
|
|
5441
|
+
* or one sign-up requires — the billing address — is missing. Carries `field`.
|
|
5442
|
+
*/
|
|
5443
|
+
readonly SUBSCRIBER_DETAIL_INVALID: "SUBSCRIBER_DETAIL_INVALID";
|
|
5444
|
+
/** A contact change named a field of the legal identity. Carries `field`. */
|
|
5445
|
+
readonly SUBSCRIBER_IDENTITY_NOT_A_CONTACT: "SUBSCRIBER_IDENTITY_NOT_A_CONTACT";
|
|
5446
|
+
readonly SUBSCRIBER_CORRECTION_REASON_REQUIRED: "SUBSCRIBER_CORRECTION_REASON_REQUIRED";
|
|
5447
|
+
readonly SUBSCRIBER_CORRECTION_ACTOR_REQUIRED: "SUBSCRIBER_CORRECTION_ACTOR_REQUIRED";
|
|
5448
|
+
readonly SUBSCRIBER_CORRECTION_CHANGES_NOTHING: "SUBSCRIBER_CORRECTION_CHANGES_NOTHING";
|
|
5449
|
+
/** The operator declared another legal entity: that is a transfer, not an edit. */
|
|
5450
|
+
readonly SUBSCRIBER_TAKEOVER_IS_A_TRANSFER: "SUBSCRIBER_TAKEOVER_IS_A_TRANSFER";
|
|
5451
|
+
};
|
|
5452
|
+
type SubscriberErrorCode = (typeof SUBSCRIBER_ERROR_CODES)[keyof typeof SUBSCRIBER_ERROR_CODES];
|
|
3795
5453
|
/** Self-service registration funnel (`PendingRegistration`). */
|
|
3796
5454
|
declare const REGISTRATION_ERROR_CODES: {
|
|
3797
5455
|
readonly PENDING_REGISTRATION_NOT_FOUND: "PENDING_REGISTRATION_NOT_FOUND";
|
|
@@ -3820,6 +5478,12 @@ declare const AUTH_ERROR_CODES: {
|
|
|
3820
5478
|
/** Neither `tenantId` nor `userId` could be resolved from the request. */
|
|
3821
5479
|
readonly TENANT_CONTEXT_MISSING: "TENANT_CONTEXT_MISSING";
|
|
3822
5480
|
readonly TENANT_ADMIN_REQUIRED: "TENANT_ADMIN_REQUIRED";
|
|
5481
|
+
/**
|
|
5482
|
+
* The tenant's billing area — its payment method, and later its invoices
|
|
5483
|
+
* and account — needs the billing permission, which the application maps
|
|
5484
|
+
* to its roles and the tenant's administrator holds by default.
|
|
5485
|
+
*/
|
|
5486
|
+
readonly BILLING_PERMISSION_REQUIRED: "BILLING_PERMISSION_REQUIRED";
|
|
3823
5487
|
readonly SUPER_ADMIN_REQUIRED: "SUPER_ADMIN_REQUIRED";
|
|
3824
5488
|
/** TOTP MFA has never been set up for this user. */
|
|
3825
5489
|
readonly MFA_NOT_SET_UP: "MFA_NOT_SET_UP";
|
|
@@ -3850,6 +5514,30 @@ declare const PROMO_ERROR_CODES: {
|
|
|
3850
5514
|
readonly PROMO_MAX_REDEMPTIONS_LOWERED: "PROMO_MAX_REDEMPTIONS_LOWERED";
|
|
3851
5515
|
};
|
|
3852
5516
|
type PromoErrorCode = (typeof PROMO_ERROR_CODES)[keyof typeof PROMO_ERROR_CODES];
|
|
5517
|
+
/** Payment methods and the gateways that confirm them. */
|
|
5518
|
+
declare const PAYMENT_ERROR_CODES: {
|
|
5519
|
+
/**
|
|
5520
|
+
* No gateway account takes new payment methods:
|
|
5521
|
+
* `config/saas.yaml#payments.newPaymentMethods` names none.
|
|
5522
|
+
*/
|
|
5523
|
+
readonly PAYMENTS_NOT_CONFIGURED: "PAYMENTS_NOT_CONFIGURED";
|
|
5524
|
+
/** A callback arrived for an account `config/saas.yaml#payments.accounts` does not name. Carries `account`. */
|
|
5525
|
+
readonly PAYMENT_GATEWAY_ACCOUNT_UNKNOWN: "PAYMENT_GATEWAY_ACCOUNT_UNKNOWN";
|
|
5526
|
+
/** A callback the gateway did not send: its signature does not verify. */
|
|
5527
|
+
readonly PAYMENT_CALLBACK_REJECTED: "PAYMENT_CALLBACK_REJECTED";
|
|
5528
|
+
/**
|
|
5529
|
+
* A success or cancel URL at an origin `config/saas.yaml#payments.returnUrlOrigins`
|
|
5530
|
+
* does not name. Carries `field`.
|
|
5531
|
+
*/
|
|
5532
|
+
readonly PAYMENT_RETURN_URL_NOT_ALLOWED: "PAYMENT_RETURN_URL_NOT_ALLOWED";
|
|
5533
|
+
};
|
|
5534
|
+
type PaymentErrorCode = (typeof PAYMENT_ERROR_CODES)[keyof typeof PAYMENT_ERROR_CODES];
|
|
5535
|
+
/** Codes of the settings record (`GET /admin/settings`, the acknowledgement). */
|
|
5536
|
+
declare const SETTINGS_ERROR_CODES: {
|
|
5537
|
+
/** No recorded change has this id, or the installation keeps no record at all. */
|
|
5538
|
+
readonly SETTINGS_CHANGE_NOT_FOUND: "SETTINGS_CHANGE_NOT_FOUND";
|
|
5539
|
+
};
|
|
5540
|
+
type SettingsErrorCode = (typeof SETTINGS_ERROR_CODES)[keyof typeof SETTINGS_ERROR_CODES];
|
|
3853
5541
|
/**
|
|
3854
5542
|
* Every exception code the platform emits, in one object.
|
|
3855
5543
|
*
|
|
@@ -3858,6 +5546,22 @@ type PromoErrorCode = (typeof PROMO_ERROR_CODES)[keyof typeof PROMO_ERROR_CODES]
|
|
|
3858
5546
|
* removing one may not.
|
|
3859
5547
|
*/
|
|
3860
5548
|
declare const PLATFORM_ERROR_CODES: {
|
|
5549
|
+
/** No recorded change has this id, or the installation keeps no record at all. */
|
|
5550
|
+
readonly SETTINGS_CHANGE_NOT_FOUND: "SETTINGS_CHANGE_NOT_FOUND";
|
|
5551
|
+
/**
|
|
5552
|
+
* No gateway account takes new payment methods:
|
|
5553
|
+
* `config/saas.yaml#payments.newPaymentMethods` names none.
|
|
5554
|
+
*/
|
|
5555
|
+
readonly PAYMENTS_NOT_CONFIGURED: "PAYMENTS_NOT_CONFIGURED";
|
|
5556
|
+
/** A callback arrived for an account `config/saas.yaml#payments.accounts` does not name. Carries `account`. */
|
|
5557
|
+
readonly PAYMENT_GATEWAY_ACCOUNT_UNKNOWN: "PAYMENT_GATEWAY_ACCOUNT_UNKNOWN";
|
|
5558
|
+
/** A callback the gateway did not send: its signature does not verify. */
|
|
5559
|
+
readonly PAYMENT_CALLBACK_REJECTED: "PAYMENT_CALLBACK_REJECTED";
|
|
5560
|
+
/**
|
|
5561
|
+
* A success or cancel URL at an origin `config/saas.yaml#payments.returnUrlOrigins`
|
|
5562
|
+
* does not name. Carries `field`.
|
|
5563
|
+
*/
|
|
5564
|
+
readonly PAYMENT_RETURN_URL_NOT_ALLOWED: "PAYMENT_RETURN_URL_NOT_ALLOWED";
|
|
3861
5565
|
readonly PENDING_REGISTRATION_NOT_FOUND: "PENDING_REGISTRATION_NOT_FOUND";
|
|
3862
5566
|
readonly PENDING_REGISTRATION_EXPIRED: "PENDING_REGISTRATION_EXPIRED";
|
|
3863
5567
|
readonly INVALID_REGISTRATION_STATE: "INVALID_REGISTRATION_STATE";
|
|
@@ -3873,20 +5577,62 @@ declare const PLATFORM_ERROR_CODES: {
|
|
|
3873
5577
|
readonly PLAN_NOT_AVAILABLE: "PLAN_NOT_AVAILABLE";
|
|
3874
5578
|
readonly PLAN_NOT_SELECTED: "PLAN_NOT_SELECTED";
|
|
3875
5579
|
readonly MODEL_NOT_AVAILABLE: "MODEL_NOT_AVAILABLE";
|
|
5580
|
+
/**
|
|
5581
|
+
* A contract, a plan change or a booking for a tenant that has no
|
|
5582
|
+
* subscriber. Nothing is agreed or charged without the party to it.
|
|
5583
|
+
* Carries `tenantId`.
|
|
5584
|
+
*/
|
|
5585
|
+
readonly SUBSCRIBER_REQUIRED: "SUBSCRIBER_REQUIRED";
|
|
5586
|
+
/** The tenant already has a live subscriber. Carries `tenantId`. */
|
|
5587
|
+
readonly SUBSCRIBER_ALREADY_EXISTS: "SUBSCRIBER_ALREADY_EXISTS";
|
|
5588
|
+
readonly SUBSCRIBER_NOT_FOUND: "SUBSCRIBER_NOT_FOUND";
|
|
5589
|
+
readonly SUBSCRIBER_LEGAL_NAME_REQUIRED: "SUBSCRIBER_LEGAL_NAME_REQUIRED";
|
|
5590
|
+
/**
|
|
5591
|
+
* A detail that has a form — the country, the invoice email — is not in it,
|
|
5592
|
+
* or one sign-up requires — the billing address — is missing. Carries `field`.
|
|
5593
|
+
*/
|
|
5594
|
+
readonly SUBSCRIBER_DETAIL_INVALID: "SUBSCRIBER_DETAIL_INVALID";
|
|
5595
|
+
/** A contact change named a field of the legal identity. Carries `field`. */
|
|
5596
|
+
readonly SUBSCRIBER_IDENTITY_NOT_A_CONTACT: "SUBSCRIBER_IDENTITY_NOT_A_CONTACT";
|
|
5597
|
+
readonly SUBSCRIBER_CORRECTION_REASON_REQUIRED: "SUBSCRIBER_CORRECTION_REASON_REQUIRED";
|
|
5598
|
+
readonly SUBSCRIBER_CORRECTION_ACTOR_REQUIRED: "SUBSCRIBER_CORRECTION_ACTOR_REQUIRED";
|
|
5599
|
+
readonly SUBSCRIBER_CORRECTION_CHANGES_NOTHING: "SUBSCRIBER_CORRECTION_CHANGES_NOTHING";
|
|
5600
|
+
/** The operator declared another legal entity: that is a transfer, not an edit. */
|
|
5601
|
+
readonly SUBSCRIBER_TAKEOVER_IS_A_TRANSFER: "SUBSCRIBER_TAKEOVER_IS_A_TRANSFER";
|
|
3876
5602
|
readonly CHECKOUT_OFFER_LINE_ITEMS_REQUIRED: "CHECKOUT_OFFER_LINE_ITEMS_REQUIRED";
|
|
3877
5603
|
readonly CHECKOUT_OFFER_PLAN_LINE_ITEM_REQUIRED: "CHECKOUT_OFFER_PLAN_LINE_ITEM_REQUIRED";
|
|
3878
5604
|
readonly CHECKOUT_OFFER_BUNDLE_LINE_ITEMS_REQUIRED: "CHECKOUT_OFFER_BUNDLE_LINE_ITEMS_REQUIRED";
|
|
3879
5605
|
readonly CHECKOUT_OFFER_BUNDLE_VERSION_NOT_BOOKABLE: "CHECKOUT_OFFER_BUNDLE_VERSION_NOT_BOOKABLE";
|
|
3880
5606
|
readonly CHECKOUT_OFFER_FEATURE_DEPENDENCY_UNSATISFIED: "CHECKOUT_OFFER_FEATURE_DEPENDENCY_UNSATISFIED";
|
|
5607
|
+
readonly CHECKOUT_OFFER_PLAN_NOT_OFFERED: "CHECKOUT_OFFER_PLAN_NOT_OFFERED";
|
|
5608
|
+
readonly CHECKOUT_OFFER_BUNDLE_NOT_OFFERED: "CHECKOUT_OFFER_BUNDLE_NOT_OFFERED";
|
|
5609
|
+
readonly CHECKOUT_OFFER_PROMO_CODE_NOT_ACCEPTED: "CHECKOUT_OFFER_PROMO_CODE_NOT_ACCEPTED";
|
|
5610
|
+
readonly CHECKOUT_OFFER_PRICE_NOT_CURRENT: "CHECKOUT_OFFER_PRICE_NOT_CURRENT";
|
|
3881
5611
|
readonly SUBSCRIPTION_CONTRACT_LINE_ITEMS_REQUIRED: "SUBSCRIPTION_CONTRACT_LINE_ITEMS_REQUIRED";
|
|
3882
5612
|
readonly SUBSCRIPTION_CONTRACT_PLAN_LINE_ITEM_REQUIRED: "SUBSCRIPTION_CONTRACT_PLAN_LINE_ITEM_REQUIRED";
|
|
3883
5613
|
readonly SUBSCRIPTION_CONTRACT_INVALID_DATE: "SUBSCRIPTION_CONTRACT_INVALID_DATE";
|
|
3884
5614
|
readonly SUBSCRIPTION_CONTRACT_INVALID_WINDOW: "SUBSCRIPTION_CONTRACT_INVALID_WINDOW";
|
|
5615
|
+
readonly SUBSCRIPTION_CONTRACT_LINE_ITEM_TAX_MISMATCH: "SUBSCRIPTION_CONTRACT_LINE_ITEM_TAX_MISMATCH";
|
|
5616
|
+
readonly SUBSCRIPTION_CONTRACT_TAX_RATE_NOT_PERCENT: "SUBSCRIPTION_CONTRACT_TAX_RATE_NOT_PERCENT";
|
|
5617
|
+
readonly SUBSCRIPTION_CONTRACT_LINE_ITEM_CURRENCY_MISMATCH: "SUBSCRIPTION_CONTRACT_LINE_ITEM_CURRENCY_MISMATCH";
|
|
5618
|
+
/**
|
|
5619
|
+
* The lines do not add up to a total the contract states, counted as often
|
|
5620
|
+
* as each falls due in one period. `field` names the total.
|
|
5621
|
+
*/
|
|
5622
|
+
readonly SUBSCRIPTION_CONTRACT_LINES_DO_NOT_ADD_UP: "SUBSCRIPTION_CONTRACT_LINES_DO_NOT_ADD_UP";
|
|
5623
|
+
/** Something that takes money off states a negative amount. */
|
|
5624
|
+
readonly SUBSCRIPTION_CONTRACT_DISCOUNT_NEGATIVE: "SUBSCRIPTION_CONTRACT_DISCOUNT_NEGATIVE";
|
|
3885
5625
|
readonly SUBSCRIPTION_CONTRACT_TERMINATION_BEFORE_START: "SUBSCRIPTION_CONTRACT_TERMINATION_BEFORE_START";
|
|
3886
5626
|
readonly CHECKOUT_OFFER_NOT_FOUND: "CHECKOUT_OFFER_NOT_FOUND";
|
|
3887
5627
|
readonly CHECKOUT_OFFER_EXPIRED: "CHECKOUT_OFFER_EXPIRED";
|
|
3888
5628
|
readonly CHECKOUT_OFFER_ALREADY_CONSUMED: "CHECKOUT_OFFER_ALREADY_CONSUMED";
|
|
3889
5629
|
readonly CHECKOUT_OFFER_NOT_CONSUMED: "CHECKOUT_OFFER_NOT_CONSUMED";
|
|
5630
|
+
/**
|
|
5631
|
+
* The offer changed between the checks of `conclude` and the transaction
|
|
5632
|
+
* that consumed it, so the contract checked is not the one the offer now
|
|
5633
|
+
* describes. Nothing was written; load the offer and conclude it again.
|
|
5634
|
+
*/
|
|
5635
|
+
readonly CHECKOUT_OFFER_CHANGED: "CHECKOUT_OFFER_CHANGED";
|
|
3890
5636
|
readonly SUBSCRIPTION_CONTRACT_NOT_FOUND: "SUBSCRIPTION_CONTRACT_NOT_FOUND";
|
|
3891
5637
|
readonly NO_ACTIVE_SUBSCRIPTION_CONTRACT: "NO_ACTIVE_SUBSCRIPTION_CONTRACT";
|
|
3892
5638
|
readonly SUBSCRIPTION_CONTRACT_ALREADY_CLOSED: "SUBSCRIPTION_CONTRACT_ALREADY_CLOSED";
|
|
@@ -3899,6 +5645,8 @@ declare const PLATFORM_ERROR_CODES: {
|
|
|
3899
5645
|
readonly BUNDLE_ALREADY_SUBSCRIBED: "BUNDLE_ALREADY_SUBSCRIBED";
|
|
3900
5646
|
readonly BUNDLE_INCOMPATIBLE_WITH_PLAN: "BUNDLE_INCOMPATIBLE_WITH_PLAN";
|
|
3901
5647
|
readonly BUNDLE_NOT_SELF_SERVICE: "BUNDLE_NOT_SELF_SERVICE";
|
|
5648
|
+
readonly BUNDLE_CYCLE_EXCEEDS_PLAN: "BUNDLE_CYCLE_EXCEEDS_PLAN";
|
|
5649
|
+
readonly BUNDLE_NOT_PRICED_FOR_THIS_PLAN: "BUNDLE_NOT_PRICED_FOR_THIS_PLAN";
|
|
3902
5650
|
readonly SUBSCRIPTION_BUNDLE_ALREADY_CANCELLED: "SUBSCRIPTION_BUNDLE_ALREADY_CANCELLED";
|
|
3903
5651
|
readonly SUBSCRIPTION_BUNDLE_NOT_CANCELLED: "SUBSCRIPTION_BUNDLE_NOT_CANCELLED";
|
|
3904
5652
|
readonly SUBSCRIPTION_BUNDLE_CANCELLATION_EFFECTIVE: "SUBSCRIPTION_BUNDLE_CANCELLATION_EFFECTIVE";
|
|
@@ -3912,8 +5660,79 @@ declare const PLATFORM_ERROR_CODES: {
|
|
|
3912
5660
|
readonly PLAN_NOT_IN_CATALOG: "PLAN_NOT_IN_CATALOG";
|
|
3913
5661
|
/** Plan exists but cannot be booked via self-service. */
|
|
3914
5662
|
readonly PLAN_NOT_SELF_SERVICE: "PLAN_NOT_SELF_SERVICE";
|
|
5663
|
+
/**
|
|
5664
|
+
* Plan carries no price for the requested billing cycle, so it is not sold
|
|
5665
|
+
* in it: a plan without a yearly price is a monthly plan.
|
|
5666
|
+
*/
|
|
5667
|
+
readonly PLAN_NOT_SOLD_IN_CYCLE: "PLAN_NOT_SOLD_IN_CYCLE";
|
|
3915
5668
|
/** Plan change refused. Carries `blockers[]` with their own codes. */
|
|
3916
5669
|
readonly PLAN_CHANGE_BLOCKED: "PLAN_CHANGE_BLOCKED";
|
|
5670
|
+
/**
|
|
5671
|
+
* The subscription moved between the read a request was decided on and the
|
|
5672
|
+
* write it attempted, so nothing was written. The caller reloads and asks
|
|
5673
|
+
* again.
|
|
5674
|
+
*/
|
|
5675
|
+
readonly SUBSCRIPTION_CHANGED: "SUBSCRIPTION_CHANGED";
|
|
5676
|
+
/**
|
|
5677
|
+
* The tenant has no subscription to act on.
|
|
5678
|
+
*
|
|
5679
|
+
* `SUBSCRIPTION_NOT_FOUND` states the same fact on the read routes. Both
|
|
5680
|
+
* are already on the wire and a code is renamed only deliberately, so both
|
|
5681
|
+
* are named here rather than one being dropped behind a consumer's back.
|
|
5682
|
+
*/
|
|
5683
|
+
readonly NO_SUBSCRIPTION: "NO_SUBSCRIPTION";
|
|
5684
|
+
/**
|
|
5685
|
+
* The cancellation date the reader was shown is no longer the one the rules
|
|
5686
|
+
* return, so the confirmation is refused rather than silently applied.
|
|
5687
|
+
* Carries the recomputed dates, so the page can re-ask instead of guessing.
|
|
5688
|
+
*/
|
|
5689
|
+
readonly CANCELLATION_TERMS_CHANGED: "CANCELLATION_TERMS_CHANGED";
|
|
5690
|
+
/** The subscription has ended; its plan can no longer be changed. */
|
|
5691
|
+
readonly SUBSCRIPTION_ENDED: "SUBSCRIPTION_ENDED";
|
|
5692
|
+
/** An active special contract blocks self-service plan changes. */
|
|
5693
|
+
readonly PLAN_LOCKED: "PLAN_LOCKED";
|
|
5694
|
+
/** Current usage of one quota exceeds what the target plan allows. */
|
|
5695
|
+
readonly QUOTA_OVER_TARGET: "QUOTA_OVER_TARGET";
|
|
5696
|
+
/** The change drops features the tenant has today. */
|
|
5697
|
+
readonly FEATURE_LOST: "FEATURE_LOST";
|
|
5698
|
+
readonly FEATURES_LOST: "FEATURES_LOST";
|
|
5699
|
+
/** Target plan and cycle already match what is in place. */
|
|
5700
|
+
readonly NO_CHANGE: "NO_CHANGE";
|
|
5701
|
+
/** A shorter cycle cannot start inside the term already running. */
|
|
5702
|
+
readonly CYCLE_SHORTENS_AT_TERM_END: "CYCLE_SHORTENS_AT_TERM_END";
|
|
5703
|
+
/** A cancelled subscription cannot change its billing cycle. */
|
|
5704
|
+
readonly CANCELLATION_LOCKS_THE_CYCLE: "CANCELLATION_LOCKS_THE_CYCLE";
|
|
5705
|
+
/**
|
|
5706
|
+
* A bundle the tenant already holds runs past the cycle they are moving to.
|
|
5707
|
+
*
|
|
5708
|
+
* Its own code rather than `BUNDLE_CYCLE_EXCEEDS_PLAN`, which states the
|
|
5709
|
+
* same rule about a booking that has not been made yet. The two need
|
|
5710
|
+
* different sentences: this one can name the day the obstacle lifts and
|
|
5711
|
+
* tell the reader to cancel the booking, and that advice is wrong for
|
|
5712
|
+
* someone who is only about to book. One template cannot serve both.
|
|
5713
|
+
*/
|
|
5714
|
+
readonly BUNDLE_BOOKING_OUTLASTS_TARGET_CYCLE: "BUNDLE_BOOKING_OUTLASTS_TARGET_CYCLE";
|
|
5715
|
+
/**
|
|
5716
|
+
* Features of the previewed bundle are already covered by the plan or by
|
|
5717
|
+
* another booked bundle. A warning rather than a blocker: paying twice is
|
|
5718
|
+
* the customer's decision, and the preview only has to say so first.
|
|
5719
|
+
*/
|
|
5720
|
+
readonly REDUNDANT_FEATURES: "REDUNDANT_FEATURES";
|
|
5721
|
+
/**
|
|
5722
|
+
* A booking's minimum term outlasts the period being cancelled, so the
|
|
5723
|
+
* cancellation takes effect at the end of the term, not of the period.
|
|
5724
|
+
*/
|
|
5725
|
+
readonly MINIMUM_TERM_BINDS: "MINIMUM_TERM_BINDS";
|
|
5726
|
+
/**
|
|
5727
|
+
* The previewed bundle requires features that neither the plan nor an
|
|
5728
|
+
* active booking provides.
|
|
5729
|
+
*
|
|
5730
|
+
* The same string is a `StrictModeWarningCode` in `bundle.types.ts`, where
|
|
5731
|
+
* it names the catalogue-authoring reading of the rule and travels with its
|
|
5732
|
+
* own message. This declaration is the booking preview's blocker, which a
|
|
5733
|
+
* tenant reads and therefore needs a shipped text for.
|
|
5734
|
+
*/
|
|
5735
|
+
readonly BUNDLE_FEATURE_DEPENDENCY_UNSATISFIED: "BUNDLE_FEATURE_DEPENDENCY_UNSATISFIED";
|
|
3917
5736
|
readonly NO_PENDING_PLAN_VERSION: "NO_PENDING_PLAN_VERSION";
|
|
3918
5737
|
readonly ONBOARDING_CREATE_FAILED: "ONBOARDING_CREATE_FAILED";
|
|
3919
5738
|
readonly BUNDLE_PREVIEW_ARGUMENT_AMBIGUOUS: "BUNDLE_PREVIEW_ARGUMENT_AMBIGUOUS";
|
|
@@ -3952,6 +5771,8 @@ declare const PLATFORM_ERROR_CODES: {
|
|
|
3952
5771
|
readonly BUNDLE_VERSION_SUPERSEDED: "BUNDLE_VERSION_SUPERSEDED";
|
|
3953
5772
|
readonly BUNDLE_VERSION_REGRESSION: "BUNDLE_VERSION_REGRESSION";
|
|
3954
5773
|
readonly BUNDLE_VERSION_ZERO_PRICE: "BUNDLE_VERSION_ZERO_PRICE";
|
|
5774
|
+
readonly BUNDLE_VERSION_NO_PRICE: "BUNDLE_VERSION_NO_PRICE";
|
|
5775
|
+
readonly BUNDLE_VERSION_NOT_PRICED_FOR_PLAN: "BUNDLE_VERSION_NOT_PRICED_FOR_PLAN";
|
|
3955
5776
|
readonly BUNDLE_VERSION_DISCARD_NOT_IMPLEMENTED: "BUNDLE_VERSION_DISCARD_NOT_IMPLEMENTED";
|
|
3956
5777
|
readonly BUNDLE_VERSION_VALID_FROM_REQUIRED: "BUNDLE_VERSION_VALID_FROM_REQUIRED";
|
|
3957
5778
|
readonly BUNDLE_VERSION_VALID_FROM_INVALID: "BUNDLE_VERSION_VALID_FROM_INVALID";
|
|
@@ -3969,6 +5790,12 @@ declare const PLATFORM_ERROR_CODES: {
|
|
|
3969
5790
|
readonly FEATURE_NOT_FOUND: "FEATURE_NOT_FOUND";
|
|
3970
5791
|
readonly QUOTA_NOT_FOUND: "QUOTA_NOT_FOUND";
|
|
3971
5792
|
readonly PROMOTION_NOT_FOUND: "PROMOTION_NOT_FOUND";
|
|
5793
|
+
/**
|
|
5794
|
+
* A promotion's value is not one its type takes: a percentage above 0 and
|
|
5795
|
+
* at most 100, an amount above 0, an intro price of at least 0 for a whole
|
|
5796
|
+
* number of months, or a whole number of free months.
|
|
5797
|
+
*/
|
|
5798
|
+
readonly PROMOTION_VALUE_INVALID: "PROMOTION_VALUE_INVALID";
|
|
3972
5799
|
readonly MARKETING_PROJECTION_NOT_FOUND: "MARKETING_PROJECTION_NOT_FOUND";
|
|
3973
5800
|
readonly PLAN_ALREADY_EXISTS: "PLAN_ALREADY_EXISTS";
|
|
3974
5801
|
readonly BUNDLE_ALREADY_EXISTS: "BUNDLE_ALREADY_EXISTS";
|
|
@@ -3979,6 +5806,10 @@ declare const PLATFORM_ERROR_CODES: {
|
|
|
3979
5806
|
readonly QUOTA_NOT_IN_DISCOVERY_SNAPSHOT: "QUOTA_NOT_IN_DISCOVERY_SNAPSHOT";
|
|
3980
5807
|
readonly DISCOVERY_STATUS_TRANSITION_INVALID: "DISCOVERY_STATUS_TRANSITION_INVALID";
|
|
3981
5808
|
readonly DISCOVERY_NOT_INITIALIZED: "DISCOVERY_NOT_INITIALIZED";
|
|
5809
|
+
/** The uploaded document is not a plan catalog — unparseable, or not an object. */
|
|
5810
|
+
readonly PLAN_CATALOG_UNREADABLE: "PLAN_CATALOG_UNREADABLE";
|
|
5811
|
+
/** It parsed, and then failed the schema or a cross-field rule. */
|
|
5812
|
+
readonly PLAN_CATALOG_INVALID: "PLAN_CATALOG_INVALID";
|
|
3982
5813
|
readonly PROMO_CODE_NOT_FOUND: "PROMO_CODE_NOT_FOUND";
|
|
3983
5814
|
readonly PROMO_CODE_ALREADY_EXISTS: "PROMO_CODE_ALREADY_EXISTS";
|
|
3984
5815
|
readonly PROMO_CODE_HAS_REDEMPTIONS: "PROMO_CODE_HAS_REDEMPTIONS";
|
|
@@ -4001,6 +5832,12 @@ declare const PLATFORM_ERROR_CODES: {
|
|
|
4001
5832
|
/** Neither `tenantId` nor `userId` could be resolved from the request. */
|
|
4002
5833
|
readonly TENANT_CONTEXT_MISSING: "TENANT_CONTEXT_MISSING";
|
|
4003
5834
|
readonly TENANT_ADMIN_REQUIRED: "TENANT_ADMIN_REQUIRED";
|
|
5835
|
+
/**
|
|
5836
|
+
* The tenant's billing area — its payment method, and later its invoices
|
|
5837
|
+
* and account — needs the billing permission, which the application maps
|
|
5838
|
+
* to its roles and the tenant's administrator holds by default.
|
|
5839
|
+
*/
|
|
5840
|
+
readonly BILLING_PERMISSION_REQUIRED: "BILLING_PERMISSION_REQUIRED";
|
|
4004
5841
|
readonly SUPER_ADMIN_REQUIRED: "SUPER_ADMIN_REQUIRED";
|
|
4005
5842
|
/** TOTP MFA has never been set up for this user. */
|
|
4006
5843
|
readonly MFA_NOT_SET_UP: "MFA_NOT_SET_UP";
|
|
@@ -4021,7 +5858,7 @@ declare const PLATFORM_ERROR_CODES: {
|
|
|
4021
5858
|
/** Email already taken (mapped from `PlatformUserExistsError`). */
|
|
4022
5859
|
readonly EMAIL_EXISTS: "EMAIL_EXISTS";
|
|
4023
5860
|
};
|
|
4024
|
-
type PlatformErrorCode = SetupErrorCode | AuthErrorCode | PromoErrorCode | CatalogErrorCode | BillingErrorCode | ContractErrorCode | RegistrationErrorCode;
|
|
5861
|
+
type PlatformErrorCode = SetupErrorCode | AuthErrorCode | PromoErrorCode | CatalogErrorCode | BillingErrorCode | ContractErrorCode | SubscriberErrorCode | RegistrationErrorCode | PaymentErrorCode | SettingsErrorCode;
|
|
4025
5862
|
/**
|
|
4026
5863
|
* Shape of a coded error response.
|
|
4027
5864
|
*
|
|
@@ -4242,7 +6079,25 @@ interface PendingRegistration {
|
|
|
4242
6079
|
billingCycle: 'MONTHLY' | 'YEARLY' | null;
|
|
4243
6080
|
/** Plaintext code (UI display). Validation runs fresh every time. */
|
|
4244
6081
|
appliedPromoCode: string | null;
|
|
6082
|
+
/**
|
|
6083
|
+
* The billing address and tax identifiers step 4 asks for, which the
|
|
6084
|
+
* subscriber is created with. The address is required before a payment
|
|
6085
|
+
* method is set up; the tax identifiers stay optional.
|
|
6086
|
+
*/
|
|
6087
|
+
addressLine1: string | null;
|
|
6088
|
+
addressLine2: string | null;
|
|
6089
|
+
postalCode: string | null;
|
|
6090
|
+
city: string | null;
|
|
6091
|
+
/** ISO 3166-1 alpha-2, upper case. */
|
|
6092
|
+
country: string | null;
|
|
6093
|
+
vatId: string | null;
|
|
6094
|
+
taxNumber: string | null;
|
|
6095
|
+
/** The gateway's session for the payment method, unique within `checkoutGatewayAccount`. */
|
|
4245
6096
|
checkoutSessionId: string | null;
|
|
6097
|
+
/** The account in `config/saas.yaml#payments.accounts` the session was opened at. */
|
|
6098
|
+
checkoutGatewayAccount: string | null;
|
|
6099
|
+
/** The customer the gateway created for the sign-up, reused when step 4 is repeated. */
|
|
6100
|
+
gatewayCustomerRef: string | null;
|
|
4246
6101
|
checkoutStartedAt: Date | null;
|
|
4247
6102
|
expiresAt: Date;
|
|
4248
6103
|
createdAt: Date;
|
|
@@ -4274,7 +6129,16 @@ interface PendingRegistrationUpdateInput {
|
|
|
4274
6129
|
configJson?: RegistrationConfigSelection | null;
|
|
4275
6130
|
billingCycle?: 'MONTHLY' | 'YEARLY' | null;
|
|
4276
6131
|
appliedPromoCode?: string | null;
|
|
6132
|
+
addressLine1?: string | null;
|
|
6133
|
+
addressLine2?: string | null;
|
|
6134
|
+
postalCode?: string | null;
|
|
6135
|
+
city?: string | null;
|
|
6136
|
+
country?: string | null;
|
|
6137
|
+
vatId?: string | null;
|
|
6138
|
+
taxNumber?: string | null;
|
|
4277
6139
|
checkoutSessionId?: string | null;
|
|
6140
|
+
checkoutGatewayAccount?: string | null;
|
|
6141
|
+
gatewayCustomerRef?: string | null;
|
|
4278
6142
|
checkoutStartedAt?: Date | null;
|
|
4279
6143
|
expiresAt?: Date;
|
|
4280
6144
|
}
|
|
@@ -4282,14 +6146,24 @@ interface PendingRegistrationUpdateInput {
|
|
|
4282
6146
|
interface PendingRegistrationRepository {
|
|
4283
6147
|
findById(id: string): Promise<PendingRegistration | null>;
|
|
4284
6148
|
findByEmail(email: string): Promise<PendingRegistration | null>;
|
|
4285
|
-
/**
|
|
4286
|
-
|
|
6149
|
+
/**
|
|
6150
|
+
* Finds the pending record a gateway session belongs to. A session
|
|
6151
|
+
* identifier is unique only within its account, so both are matched.
|
|
6152
|
+
*/
|
|
6153
|
+
findByCheckoutSession(gatewayAccount: string, sessionId: string): Promise<PendingRegistration | null>;
|
|
4287
6154
|
/**
|
|
4288
6155
|
* Cleanup lookup: all pending records with `expiresAt < now`, max
|
|
4289
6156
|
* `limit` entries per call (batch protection). Ordering irrelevant, the
|
|
4290
6157
|
* cron service iterates sequentially.
|
|
4291
6158
|
*/
|
|
4292
6159
|
findExpired(now: Date, limit: number): Promise<PendingRegistration[]>;
|
|
6160
|
+
/**
|
|
6161
|
+
* Every gateway account a checkout session is still open at: the distinct
|
|
6162
|
+
* `checkoutGatewayAccount` of records in `CHECKOUT_STARTED` whose
|
|
6163
|
+
* `expiresAt` is after `now`. The start refuses when one of them is no
|
|
6164
|
+
* longer configured, because that sign-up's confirmation could not arrive.
|
|
6165
|
+
*/
|
|
6166
|
+
findOpenCheckoutAccounts(now: Date): Promise<string[]>;
|
|
4293
6167
|
create(input: PendingRegistrationCreateInput): Promise<PendingRegistration>;
|
|
4294
6168
|
update(id: string, input: PendingRegistrationUpdateInput): Promise<PendingRegistration>;
|
|
4295
6169
|
/**
|
|
@@ -4299,7 +6173,13 @@ interface PendingRegistrationRepository {
|
|
|
4299
6173
|
* value is the authoritative threshold for the lockout check.
|
|
4300
6174
|
*/
|
|
4301
6175
|
incrementOtpAttemptCount(id: string): Promise<number>;
|
|
4302
|
-
|
|
6176
|
+
/**
|
|
6177
|
+
* Removes the record. With `tx` it is removed on that transaction and comes
|
|
6178
|
+
* back with its rollback — an activation deletes the sign-up on the
|
|
6179
|
+
* transaction that creates the tenant, so a sign-up is either still waiting
|
|
6180
|
+
* or activated, never both.
|
|
6181
|
+
*/
|
|
6182
|
+
delete(id: string, tx?: TransactionContext): Promise<void>;
|
|
4303
6183
|
}
|
|
4304
6184
|
/** Adapter port: detects whether a full user account (verified) exists for this email. */
|
|
4305
6185
|
interface UserAccountLookup {
|
|
@@ -4309,67 +6189,42 @@ interface UserAccountLookup {
|
|
|
4309
6189
|
interface SlugAvailabilityCheck {
|
|
4310
6190
|
isSlugAvailable(slug: string): Promise<boolean>;
|
|
4311
6191
|
}
|
|
4312
|
-
/** Wire format of a created checkout session (provider-agnostic). */
|
|
4313
|
-
interface CheckoutSession {
|
|
4314
|
-
/** Provider-specific session ID (e.g. Stripe `cs_…`). */
|
|
4315
|
-
sessionId: string;
|
|
4316
|
-
/** Payment URL to be opened by the frontend. */
|
|
4317
|
-
checkoutUrl: string;
|
|
4318
|
-
/** Optional: provider name (`stripe`, `dev-stub`) for logging/audit. */
|
|
4319
|
-
provider?: string;
|
|
4320
|
-
}
|
|
4321
|
-
type PaymentEventStatus = 'SUCCEEDED' | 'FAILED';
|
|
4322
|
-
/**
|
|
4323
|
-
* Adapter port: idempotency log for payment webhooks. Stripe (and most
|
|
4324
|
-
* other providers) deliver events at-least-once — the service calls
|
|
4325
|
-
* `tryClaim` as an atomic race guard BEFORE it triggers the final
|
|
4326
|
-
* activation.
|
|
4327
|
-
*/
|
|
4328
|
-
interface PaymentEventLog {
|
|
4329
|
-
/**
|
|
4330
|
-
* Tries to insert an event record via `@unique` INSERT. Returns
|
|
4331
|
-
* `true` if it was newly created (webhook seen for the first time),
|
|
4332
|
-
* `false` if it already exists (duplicate → silently drop).
|
|
4333
|
-
*
|
|
4334
|
-
* Implementations must return a DB unique-constraint-violation error
|
|
4335
|
-
* (Prisma P2002) as `false`.
|
|
4336
|
-
*/
|
|
4337
|
-
tryClaim(eventId: string, payload: {
|
|
4338
|
-
provider: string;
|
|
4339
|
-
sessionId: string | null;
|
|
4340
|
-
status: PaymentEventStatus;
|
|
4341
|
-
rawPayload?: unknown;
|
|
4342
|
-
}): Promise<boolean>;
|
|
4343
|
-
}
|
|
4344
6192
|
interface FinalActivationResult {
|
|
4345
6193
|
userId: string;
|
|
4346
6194
|
tenantId: string;
|
|
4347
6195
|
subscriptionId: string;
|
|
6196
|
+
/**
|
|
6197
|
+
* The subscriber created for the tenant in the same transaction — the party
|
|
6198
|
+
* its contracts are concluded with. `subscriberFromRegistration(pending)`
|
|
6199
|
+
* says what it is created with.
|
|
6200
|
+
*/
|
|
6201
|
+
subscriberId: string;
|
|
6202
|
+
}
|
|
6203
|
+
/** The transaction a sign-up is activated on. */
|
|
6204
|
+
interface RegistrationActivation {
|
|
6205
|
+
/**
|
|
6206
|
+
* Opened by the platform, which has already claimed the gateway's
|
|
6207
|
+
* confirmation on it and records the confirmed payment method on it once
|
|
6208
|
+
* `activate` returns. Every row the activation writes goes through it, so
|
|
6209
|
+
* a failure anywhere rolls back all of it — the claim included, and the
|
|
6210
|
+
* gateway's retry is handled rather than discarded as a duplicate.
|
|
6211
|
+
*/
|
|
6212
|
+
tx: TransactionContext;
|
|
4348
6213
|
}
|
|
4349
6214
|
/**
|
|
4350
|
-
* Adapter port:
|
|
4351
|
-
*
|
|
4352
|
-
* own schema (e.g. Tenant + TenantUser + Role + UserRole +
|
|
4353
|
-
* Subscription).
|
|
6215
|
+
* Adapter port: creates User + Tenant + Subscriber + Subscription once the
|
|
6216
|
+
* gateway confirmed the sign-up's payment method. App-specific — each app has
|
|
6217
|
+
* its own schema (e.g. Tenant + TenantUser + Role + UserRole + Subscription).
|
|
4354
6218
|
*
|
|
4355
|
-
* Implementations
|
|
4356
|
-
*
|
|
6219
|
+
* Implementations write on `activation.tx` and open no transaction of their
|
|
6220
|
+
* own: a write beside it would survive the rollback that undoes the rest. The
|
|
6221
|
+
* subscriber is created there too, before any contract:
|
|
6222
|
+
* `SubscriberService.createForTenant(tenantId, subscriberFromRegistration(pending),
|
|
6223
|
+
* activation.tx)`, or `CheckoutOfferService.conclude` with `subscriber` and
|
|
6224
|
+
* that transaction.
|
|
4357
6225
|
*/
|
|
4358
6226
|
interface ActivationOrchestrator {
|
|
4359
|
-
activate(pending: PendingRegistration): Promise<FinalActivationResult>;
|
|
4360
|
-
}
|
|
4361
|
-
interface HandlePaymentEventInput {
|
|
4362
|
-
eventId: string;
|
|
4363
|
-
sessionId: string | null;
|
|
4364
|
-
provider: string;
|
|
4365
|
-
status: PaymentEventStatus;
|
|
4366
|
-
rawPayload?: unknown;
|
|
4367
|
-
}
|
|
4368
|
-
type HandlePaymentEventReason = 'ALREADY_PROCESSED' | 'PAYMENT_NOT_SUCCEEDED' | 'MISSING_SESSION_ID' | 'PENDING_REGISTRATION_NOT_FOUND' | 'INVALID_STATE';
|
|
4369
|
-
interface HandlePaymentEventResult {
|
|
4370
|
-
activated: boolean;
|
|
4371
|
-
reason?: HandlePaymentEventReason;
|
|
4372
|
-
result?: FinalActivationResult;
|
|
6227
|
+
activate(pending: PendingRegistration, activation: RegistrationActivation): Promise<FinalActivationResult>;
|
|
4373
6228
|
}
|
|
4374
6229
|
interface CleanupResult {
|
|
4375
6230
|
/** Number of deleted PendingRegistration records. */
|
|
@@ -4380,7 +6235,7 @@ interface CleanupResult {
|
|
|
4380
6235
|
*/
|
|
4381
6236
|
moreAvailable: boolean;
|
|
4382
6237
|
}
|
|
4383
|
-
type RegistrationAuditEventType = 'REGISTRATION_STARTED' | 'REGISTRATION_NEUTRAL_ACTIVE_USER' | 'REGISTRATION_NEUTRAL_REPLAY' | 'REGISTRATION_NEUTRAL_EXPIRED' | 'OTP_VERIFIED' | 'OTP_VERIFY_FAILED' | 'OTP_RESEND_REQUESTED' | 'OTP_RATE_LIMIT_HIT' | 'PLAN_SELECTED' | 'CHECKOUT_STARTED' | 'PAYMENT_RECEIVED' | '
|
|
6238
|
+
type RegistrationAuditEventType = 'REGISTRATION_STARTED' | 'REGISTRATION_NEUTRAL_ACTIVE_USER' | 'REGISTRATION_NEUTRAL_REPLAY' | 'REGISTRATION_NEUTRAL_EXPIRED' | 'OTP_VERIFIED' | 'OTP_VERIFY_FAILED' | 'OTP_RESEND_REQUESTED' | 'OTP_RATE_LIMIT_HIT' | 'PLAN_SELECTED' | 'CHECKOUT_STARTED' | 'PAYMENT_RECEIVED' | 'PAYMENT_FAILED' | 'ACTIVATION_COMPLETED' | 'LOGIN_SUCCEEDED' | 'LOGIN_INVALID_CREDENTIALS' | 'LOGIN_ONBOARDING_REQUIRED';
|
|
4384
6239
|
/**
|
|
4385
6240
|
* Context information that the audit layer records per event.
|
|
4386
6241
|
* IP is expected as a hashed fingerprint — no plaintext IPs in the
|
|
@@ -4431,8 +6286,6 @@ interface ConfiguratorModel {
|
|
|
4431
6286
|
popular?: boolean;
|
|
4432
6287
|
}
|
|
4433
6288
|
interface ConfiguratorCatalog {
|
|
4434
|
-
/** Factor `yearlyNet = monthlyNet * cycleDiscount` (typically 10 = 2 months free). */
|
|
4435
|
-
cycleDiscount: number;
|
|
4436
6289
|
currency: string;
|
|
4437
6290
|
vatRate: number;
|
|
4438
6291
|
models: ConfiguratorModel[];
|
|
@@ -4457,6 +6310,10 @@ interface ConfiguratorPriceBreakdown {
|
|
|
4457
6310
|
modelMonthlyNet: number;
|
|
4458
6311
|
subtotalMonthlyNet: number;
|
|
4459
6312
|
subtotalNet: number;
|
|
6313
|
+
/**
|
|
6314
|
+
* Net amount taken off `subtotalNet`: the promo preview's gross discount
|
|
6315
|
+
* converted at `vatRate`.
|
|
6316
|
+
*/
|
|
4460
6317
|
discountAmount: number;
|
|
4461
6318
|
totalNet: number;
|
|
4462
6319
|
vatRate: number;
|
|
@@ -4533,8 +6390,6 @@ interface ConfiguratorPlanMarketing {
|
|
|
4533
6390
|
*/
|
|
4534
6391
|
interface ConfiguratorMarketingProvider {
|
|
4535
6392
|
listPlanMarketing(): ConfiguratorPlanMarketing[];
|
|
4536
|
-
/** Factor `yearlyNet = monthlyNet * cycleDiscount`. Default `10`. */
|
|
4537
|
-
getCycleDiscount(): number;
|
|
4538
6393
|
getVatRate(): number;
|
|
4539
6394
|
getCurrency(): string;
|
|
4540
6395
|
}
|
|
@@ -4555,6 +6410,7 @@ interface RegistrationPromoPreview {
|
|
|
4555
6410
|
reason?: string;
|
|
4556
6411
|
percent?: number;
|
|
4557
6412
|
label?: string;
|
|
6413
|
+
/** The discount in gross, reckoned against `subtotalGross`. */
|
|
4558
6414
|
discountAmount?: number;
|
|
4559
6415
|
}>;
|
|
4560
6416
|
}
|
|
@@ -4626,6 +6482,8 @@ interface PendingRegistrationSnapshot {
|
|
|
4626
6482
|
config: RegistrationConfigSelection | null;
|
|
4627
6483
|
billingCycle: 'MONTHLY' | 'YEARLY' | null;
|
|
4628
6484
|
appliedPromoCode: string | null;
|
|
6485
|
+
/** The billing details step 4 already took, to fill its form again. */
|
|
6486
|
+
billingDetails: RegistrationBillingDetails | null;
|
|
4629
6487
|
checkoutSessionId: string | null;
|
|
4630
6488
|
}
|
|
4631
6489
|
interface ResumeRegistrationResult {
|
|
@@ -4634,27 +6492,6 @@ interface ResumeRegistrationResult {
|
|
|
4634
6492
|
nextStep: RegistrationStep;
|
|
4635
6493
|
snapshot: PendingRegistrationSnapshot;
|
|
4636
6494
|
}
|
|
4637
|
-
/** Adapter port: payment provider (Stripe, Dev-Stub, Mollie, ...). */
|
|
4638
|
-
interface PaymentProvider {
|
|
4639
|
-
/**
|
|
4640
|
-
* Creates a checkout session at the payment provider and returns the URL
|
|
4641
|
-
* that the frontend should redirect to.
|
|
4642
|
-
*
|
|
4643
|
-
* @param params.pendingRegistrationId Stored as `client_reference_id` (or similar)
|
|
4644
|
-
* in the provider — the webhook needs it to link back.
|
|
4645
|
-
* @param params.planId The chosen plan (Stripe price/product mapping lives
|
|
4646
|
-
* in the adapter).
|
|
4647
|
-
* @param params.successUrl Where to go after successful payment.
|
|
4648
|
-
* @param params.cancelUrl Where to go on cancellation.
|
|
4649
|
-
*/
|
|
4650
|
-
createCheckoutSession(params: {
|
|
4651
|
-
pendingRegistrationId: string;
|
|
4652
|
-
planId: string;
|
|
4653
|
-
email: string;
|
|
4654
|
-
successUrl: string;
|
|
4655
|
-
cancelUrl: string;
|
|
4656
|
-
}): Promise<CheckoutSession>;
|
|
4657
|
-
}
|
|
4658
6495
|
/** Adapter port: OTP delivery via email (or another channel). */
|
|
4659
6496
|
interface RegistrationOtpDelivery {
|
|
4660
6497
|
sendVerificationOtp(params: {
|
|
@@ -4720,10 +6557,38 @@ interface SelectPlanResult {
|
|
|
4720
6557
|
nextStep: RegistrationStep;
|
|
4721
6558
|
selectedPlanId: string;
|
|
4722
6559
|
}
|
|
6560
|
+
/**
|
|
6561
|
+
* The billing address and tax identifiers a sign-up gives in step 4, which its
|
|
6562
|
+
* subscriber is created with. The address is required; the tax identifiers
|
|
6563
|
+
* stay optional until the tax adapter says when one is needed.
|
|
6564
|
+
*/
|
|
6565
|
+
interface RegistrationBillingDetails {
|
|
6566
|
+
addressLine1: string;
|
|
6567
|
+
addressLine2?: string | null;
|
|
6568
|
+
postalCode: string;
|
|
6569
|
+
city: string;
|
|
6570
|
+
/** ISO 3166-1 alpha-2, upper case. */
|
|
6571
|
+
country: string;
|
|
6572
|
+
vatId?: string | null;
|
|
6573
|
+
taxNumber?: string | null;
|
|
6574
|
+
}
|
|
4723
6575
|
interface StartCheckoutInput {
|
|
4724
6576
|
pendingRegistrationId: string;
|
|
6577
|
+
billingDetails: RegistrationBillingDetails;
|
|
6578
|
+
/** Where the gateway's form sends the person once the payment method is set up. */
|
|
4725
6579
|
successUrl: string;
|
|
6580
|
+
/** Where the gateway's form sends the person who leaves it. */
|
|
4726
6581
|
cancelUrl: string;
|
|
6582
|
+
/**
|
|
6583
|
+
* The checkout offer the sign-up concludes on activation. When it carries a
|
|
6584
|
+
* promo code, a slot of that code is held for it from this step until a
|
|
6585
|
+
* confirmation of the gateway's form can no longer arrive
|
|
6586
|
+
* (`PaymentMethodSetupSession.confirmableUntil`), so the code cannot run
|
|
6587
|
+
* out between this step and the payment confirmation; a code that cannot
|
|
6588
|
+
* be held refuses the step before the gateway's form opens. Left out,
|
|
6589
|
+
* nothing is held.
|
|
6590
|
+
*/
|
|
6591
|
+
checkoutOfferId?: string | null;
|
|
4727
6592
|
}
|
|
4728
6593
|
interface StartCheckoutResult {
|
|
4729
6594
|
pendingRegistrationId: string;
|
|
@@ -4766,6 +6631,344 @@ interface SetupConfirmMfaResponse {
|
|
|
4766
6631
|
ok: boolean;
|
|
4767
6632
|
}
|
|
4768
6633
|
|
|
6634
|
+
/** What the adapter's schema can actually answer about a version's dates. */
|
|
6635
|
+
interface PlanVersionMappingFields {
|
|
6636
|
+
/** `validFrom`/`validUntil` are maintained; otherwise both read as null. */
|
|
6637
|
+
validityWindows: boolean;
|
|
6638
|
+
/** `endsAt` exists; otherwise the field is left off the record entirely. */
|
|
6639
|
+
endsAt: boolean;
|
|
6640
|
+
}
|
|
6641
|
+
/** A `plans` row as either adapter reads it back. */
|
|
6642
|
+
interface CanonicalPlanRow {
|
|
6643
|
+
id: string;
|
|
6644
|
+
planKey: string;
|
|
6645
|
+
label: string;
|
|
6646
|
+
description: string | null;
|
|
6647
|
+
icon: string | null;
|
|
6648
|
+
sortOrder: number;
|
|
6649
|
+
createdAt: Date;
|
|
6650
|
+
updatedAt: Date;
|
|
6651
|
+
deletedAt: Date | null;
|
|
6652
|
+
}
|
|
6653
|
+
/** A `plan_versions` row as either adapter reads it back. */
|
|
6654
|
+
interface CanonicalPlanVersionRow {
|
|
6655
|
+
id: string;
|
|
6656
|
+
version: number;
|
|
6657
|
+
baseVersionId: string | null;
|
|
6658
|
+
features: unknown;
|
|
6659
|
+
quotas: unknown;
|
|
6660
|
+
monthlyNet: unknown;
|
|
6661
|
+
yearlyNet: unknown;
|
|
6662
|
+
marketed: boolean;
|
|
6663
|
+
publishedAt: Date | null;
|
|
6664
|
+
supersededAt: Date | null;
|
|
6665
|
+
publishedChanges: unknown;
|
|
6666
|
+
changeNote: string;
|
|
6667
|
+
nonRegressive: boolean;
|
|
6668
|
+
validFrom?: Date | null;
|
|
6669
|
+
validUntil?: Date | null;
|
|
6670
|
+
endsAt?: Date | null;
|
|
6671
|
+
createdByUserId: string | null;
|
|
6672
|
+
publishedByUserId: string | null;
|
|
6673
|
+
createdAt: Date;
|
|
6674
|
+
updatedAt: Date;
|
|
6675
|
+
}
|
|
6676
|
+
declare function toPlanRow(row: CanonicalPlanRow): PlanRow;
|
|
6677
|
+
/**
|
|
6678
|
+
* `planKey` is passed rather than read off the row: the canonical schema stores
|
|
6679
|
+
* the plan key in `planId`, but an adapter translating a consumer schema with a
|
|
6680
|
+
* real foreign key has to resolve it first, and only the adapter knows which
|
|
6681
|
+
* shape it is looking at.
|
|
6682
|
+
*/
|
|
6683
|
+
declare function toPlanVersionRow(row: CanonicalPlanVersionRow, planKey: string, fields: PlanVersionMappingFields): PlanVersionRow;
|
|
6684
|
+
|
|
6685
|
+
/**
|
|
6686
|
+
* The legal identity of the issuer a catalogue names, or none where it names no
|
|
6687
|
+
* issuer.
|
|
6688
|
+
*
|
|
6689
|
+
* Settled through the same reader as the recorded side, and that symmetry is
|
|
6690
|
+
* the point rather than tidiness: the record is a verbatim copy of the file, so
|
|
6691
|
+
* a value whose two sides were settled differently would differ from itself. A
|
|
6692
|
+
* legal name written with a trailing space would then refuse the SECOND start on
|
|
6693
|
+
* a file nobody touched, and no declaration could make it stop happening.
|
|
6694
|
+
*/
|
|
6695
|
+
declare function issuerIdentityOf(issuer: PlanCatalogIssuer | undefined): LegalIdentity | null;
|
|
6696
|
+
/**
|
|
6697
|
+
* The issuer identity a recorded settings tree holds.
|
|
6698
|
+
*
|
|
6699
|
+
* Read defensively rather than cast: the record is JSON as some earlier version
|
|
6700
|
+
* of this platform wrote it, and a tree without an issuer, or with one whose
|
|
6701
|
+
* legal name is not a name, is read as "no identity was recorded" — which is
|
|
6702
|
+
* the first naming, not a change. The schema keeps a name of only whitespace out
|
|
6703
|
+
* of the file; this keeps one out of a record written before it did.
|
|
6704
|
+
*/
|
|
6705
|
+
declare function recordedIssuerIdentity(settings: AppliedSettingsValues | null | undefined): LegalIdentity | null;
|
|
6706
|
+
/** Why `issuer.correctionOf` does not cover the change it is there to declare. */
|
|
6707
|
+
type IssuerCorrectionFault =
|
|
6708
|
+
/** There is no declaration at all. */
|
|
6709
|
+
{
|
|
6710
|
+
kind: 'absent';
|
|
6711
|
+
}
|
|
6712
|
+
/**
|
|
6713
|
+
* The file names no issuer for a declaration to be about — the block is
|
|
6714
|
+
* gone, or it is there with a name that reads as nothing. Never a
|
|
6715
|
+
* correction, whatever is declared: there is no entity on this side for the
|
|
6716
|
+
* recorded one to be the same as.
|
|
6717
|
+
*/
|
|
6718
|
+
| {
|
|
6719
|
+
kind: 'names-no-issuer';
|
|
6720
|
+
}
|
|
6721
|
+
/**
|
|
6722
|
+
* It names a value the record does not hold. Either the declaration is
|
|
6723
|
+
* stale — it belongs to a correction already applied — or it is about
|
|
6724
|
+
* another entity than the one this installation recorded.
|
|
6725
|
+
*/
|
|
6726
|
+
| {
|
|
6727
|
+
kind: 'names-another-value';
|
|
6728
|
+
field: LegalIdentityField;
|
|
6729
|
+
declared: string | null;
|
|
6730
|
+
recorded: string | null;
|
|
6731
|
+
}
|
|
6732
|
+
/** It says nothing about a field the change moves, so that field is undeclared. */
|
|
6733
|
+
| {
|
|
6734
|
+
kind: 'leaves-a-field-out';
|
|
6735
|
+
field: LegalIdentityField;
|
|
6736
|
+
recorded: string | null;
|
|
6737
|
+
};
|
|
6738
|
+
/** What a start finds when it compares the file's issuer with the recorded one. */
|
|
6739
|
+
type IssuerIdentityChange =
|
|
6740
|
+
/** Neither the record nor the file names an issuer. */
|
|
6741
|
+
{
|
|
6742
|
+
kind: 'none-named';
|
|
6743
|
+
}
|
|
6744
|
+
/** The same entity, whatever the address and the contact details did. */
|
|
6745
|
+
| {
|
|
6746
|
+
kind: 'unchanged';
|
|
6747
|
+
identity: LegalIdentity;
|
|
6748
|
+
}
|
|
6749
|
+
/**
|
|
6750
|
+
* The first issuer this installation names. No contract can have been
|
|
6751
|
+
* concluded under another one, so nothing is declared for it.
|
|
6752
|
+
*/
|
|
6753
|
+
| {
|
|
6754
|
+
kind: 'first-naming';
|
|
6755
|
+
identity: LegalIdentity;
|
|
6756
|
+
}
|
|
6757
|
+
/** The same entity, corrected as the file declares. `current` is never absent. */
|
|
6758
|
+
| {
|
|
6759
|
+
kind: 'corrected';
|
|
6760
|
+
recorded: LegalIdentity;
|
|
6761
|
+
current: LegalIdentity;
|
|
6762
|
+
moved: readonly LegalIdentityField[];
|
|
6763
|
+
reason: string;
|
|
6764
|
+
}
|
|
6765
|
+
/** Another identity, with no declaration that covers it. Refused. */
|
|
6766
|
+
| {
|
|
6767
|
+
kind: 'undeclared';
|
|
6768
|
+
recorded: LegalIdentity;
|
|
6769
|
+
/** `null` where the file names no issuer that has a name — see `names-no-issuer`. */
|
|
6770
|
+
current: LegalIdentity | null;
|
|
6771
|
+
moved: readonly LegalIdentityField[];
|
|
6772
|
+
fault: IssuerCorrectionFault;
|
|
6773
|
+
};
|
|
6774
|
+
/**
|
|
6775
|
+
* What the issuer in the file is, against the identity the record holds.
|
|
6776
|
+
*
|
|
6777
|
+
* `recorded` is `null` on the very first start, and on an installation that
|
|
6778
|
+
* has never named an issuer.
|
|
6779
|
+
*/
|
|
6780
|
+
declare function classifyIssuerChange(recorded: LegalIdentity | null, issuer: PlanCatalogIssuer | undefined): IssuerIdentityChange;
|
|
6781
|
+
|
|
6782
|
+
/** A `subscribers` row as either adapter reads it back. */
|
|
6783
|
+
interface CanonicalSubscriberRow {
|
|
6784
|
+
id: string;
|
|
6785
|
+
customerSequence: number;
|
|
6786
|
+
customerNumberPrefix: string;
|
|
6787
|
+
legalName: string;
|
|
6788
|
+
addressLine1: string | null;
|
|
6789
|
+
addressLine2: string | null;
|
|
6790
|
+
postalCode: string | null;
|
|
6791
|
+
city: string | null;
|
|
6792
|
+
country: string | null;
|
|
6793
|
+
vatId: string | null;
|
|
6794
|
+
taxNumber: string | null;
|
|
6795
|
+
invoiceEmail: string | null;
|
|
6796
|
+
migrated: boolean;
|
|
6797
|
+
createdAt: Date;
|
|
6798
|
+
updatedAt: Date;
|
|
6799
|
+
}
|
|
6800
|
+
/** A `subscriber_corrections` row as either adapter reads it back. */
|
|
6801
|
+
interface CanonicalSubscriberCorrectionRow {
|
|
6802
|
+
id: string;
|
|
6803
|
+
subscriberId: string;
|
|
6804
|
+
previous: unknown;
|
|
6805
|
+
corrected: unknown;
|
|
6806
|
+
reason: string;
|
|
6807
|
+
correctedBy: string;
|
|
6808
|
+
correctedAt: Date;
|
|
6809
|
+
}
|
|
6810
|
+
/**
|
|
6811
|
+
* The customer number a subscriber is known by: the prefix it was assigned
|
|
6812
|
+
* with, then the number. Kept apart in storage so the number orders and the
|
|
6813
|
+
* prefix stays what it was on the day it was assigned.
|
|
6814
|
+
*/
|
|
6815
|
+
declare function formatCustomerNumber(prefix: string, sequence: number): string;
|
|
6816
|
+
/** `tenantId` is the tenant the subscriber's live link names, read beside the row. */
|
|
6817
|
+
declare function toSubscriberRecord(row: CanonicalSubscriberRow, tenantId: string | null): SubscriberRecord;
|
|
6818
|
+
declare function toSubscriberCorrectionRecord(row: CanonicalSubscriberCorrectionRow): SubscriberCorrectionRecord;
|
|
6819
|
+
/**
|
|
6820
|
+
* The part of a correction that changes something: every corrected field whose
|
|
6821
|
+
* stored value differs, with the value it replaces. Both adapters decide this
|
|
6822
|
+
* the same way, under the lock they read `current` with.
|
|
6823
|
+
*/
|
|
6824
|
+
declare function identityCorrectionDelta(current: Pick<SubscriberRecord, LegalIdentityField>, corrected: SubscriberIdentityValues): SubscriberIdentityDelta;
|
|
6825
|
+
/**
|
|
6826
|
+
* Who a new contract is between: the subscriber as it stands, and the issuer as
|
|
6827
|
+
* the running configuration names it — or no issuer, where it names none.
|
|
6828
|
+
*/
|
|
6829
|
+
declare function contractPartiesOf(subscriber: SubscriberRecord, issuer: PlanCatalog['issuer']): SubscriptionContractParties;
|
|
6830
|
+
/**
|
|
6831
|
+
* The subscriber a completed sign-up is created with: the name the tenant was
|
|
6832
|
+
* registered under as its legal name, the address the registration was
|
|
6833
|
+
* verified with as its invoice email, and the billing address and tax
|
|
6834
|
+
* identifiers step 4 took.
|
|
6835
|
+
*/
|
|
6836
|
+
declare function subscriberFromRegistration(pending: Pick<PendingRegistration, 'tenantName' | 'email' | 'addressLine1' | 'addressLine2' | 'postalCode' | 'city' | 'country' | 'vatId' | 'taxNumber'>): NewSubscriberDetails;
|
|
6837
|
+
|
|
6838
|
+
/** The payment method types, in the order a form offers them. */
|
|
6839
|
+
declare const PAYMENT_METHOD_TYPES: readonly PaymentMethodType[];
|
|
6840
|
+
/** A `subscriber_payment_methods` row as either adapter reads it back. */
|
|
6841
|
+
interface CanonicalSubscriberPaymentMethodRow {
|
|
6842
|
+
id: string;
|
|
6843
|
+
subscriberId: string;
|
|
6844
|
+
gatewayAccount: string;
|
|
6845
|
+
provider: string;
|
|
6846
|
+
customerRef: string;
|
|
6847
|
+
paymentMethodRef: string;
|
|
6848
|
+
type: string;
|
|
6849
|
+
brand: string | null;
|
|
6850
|
+
last4: string;
|
|
6851
|
+
expiryMonth: number | null;
|
|
6852
|
+
expiryYear: number | null;
|
|
6853
|
+
country: string | null;
|
|
6854
|
+
bankCode: string | null;
|
|
6855
|
+
mandateReference: string | null;
|
|
6856
|
+
status: string;
|
|
6857
|
+
confirmedAt: Date;
|
|
6858
|
+
replacedAt: Date | null;
|
|
6859
|
+
createdAt: Date;
|
|
6860
|
+
}
|
|
6861
|
+
/**
|
|
6862
|
+
* Reads a row back as a record.
|
|
6863
|
+
*
|
|
6864
|
+
* The two text columns with a closed set of values are checked rather than
|
|
6865
|
+
* cast: a value written by hand or by an older release would otherwise reach a
|
|
6866
|
+
* screen as a type nobody handles.
|
|
6867
|
+
*/
|
|
6868
|
+
declare function toSubscriberPaymentMethodRecord(row: CanonicalSubscriberPaymentMethodRow): SubscriberPaymentMethodRecord;
|
|
6869
|
+
/** The columns a confirmed payment method is written with, and nothing a caller added beside them. */
|
|
6870
|
+
declare function subscriberPaymentMethodColumns(data: RecordSubscriberPaymentMethodData): Omit<CanonicalSubscriberPaymentMethodRow, 'id' | 'status' | 'replacedAt' | 'createdAt'>;
|
|
6871
|
+
/**
|
|
6872
|
+
* A confirmation naming a reference that belongs to another subscriber.
|
|
6873
|
+
*
|
|
6874
|
+
* `(gatewayAccount, paymentMethodRef)` is unique account-wide rather than per
|
|
6875
|
+
* subscriber, and that is what makes the reference one subscriber's for good:
|
|
6876
|
+
* the second row cannot be written. This is the same boundary drawn for the
|
|
6877
|
+
* caller, so that a confirmation is refused rather than answered as the
|
|
6878
|
+
* duplicate of a payment method that is not this subscriber's — on a gateway
|
|
6879
|
+
* callback, which arrives without a session and with the tenant policy lifted,
|
|
6880
|
+
* so nothing else there bounds the question.
|
|
6881
|
+
*
|
|
6882
|
+
* That a provider issues a reference once per payer is a property of that
|
|
6883
|
+
* provider and no promise of this platform's, which is why the condition is
|
|
6884
|
+
* checked rather than assumed.
|
|
6885
|
+
*
|
|
6886
|
+
* It names neither the subscriber that holds the reference nor the tenant
|
|
6887
|
+
* behind it: whoever reads the log of the refused confirmation is on the other
|
|
6888
|
+
* side of the boundary this refusal draws. The account and the reference are
|
|
6889
|
+
* carried as fields so that a caller can say which confirmation was refused
|
|
6890
|
+
* without taking the sentence apart.
|
|
6891
|
+
*/
|
|
6892
|
+
declare class ForeignPaymentMethodReferenceError extends Error {
|
|
6893
|
+
readonly gatewayAccount: string;
|
|
6894
|
+
readonly paymentMethodRef: string;
|
|
6895
|
+
readonly code = "FOREIGN_PAYMENT_METHOD_REFERENCE";
|
|
6896
|
+
constructor(gatewayAccount: string, paymentMethodRef: string);
|
|
6897
|
+
}
|
|
6898
|
+
/** Realm-safe type guard, like `isPaymentCallbackRejectedError`. */
|
|
6899
|
+
declare function isForeignPaymentMethodReferenceError(error: unknown): error is ForeignPaymentMethodReferenceError;
|
|
6900
|
+
/**
|
|
6901
|
+
* Refuses a reference of `gatewayAccount` that another subscriber already
|
|
6902
|
+
* holds, where the row could be read.
|
|
6903
|
+
*
|
|
6904
|
+
* A write cannot rely on this alone: a read does not see a row a concurrent
|
|
6905
|
+
* transaction has not committed. What the reference's unique key refuses is
|
|
6906
|
+
* refused with the same error — see `SubscriberPaymentMethodRepository`.
|
|
6907
|
+
*/
|
|
6908
|
+
declare function refuseForeignPaymentMethodReference(recorded: Pick<CanonicalSubscriberPaymentMethodRow, 'subscriberId' | 'gatewayAccount' | 'paymentMethodRef'>, subscriberId: string): void;
|
|
6909
|
+
|
|
6910
|
+
/** A `subscription_contracts` row as either adapter reads it back. */
|
|
6911
|
+
interface CanonicalContractRow {
|
|
6912
|
+
id: string;
|
|
6913
|
+
tenantId: string;
|
|
6914
|
+
subscriberId: string;
|
|
6915
|
+
subscriberSnapshot: unknown;
|
|
6916
|
+
issuerSnapshot: unknown;
|
|
6917
|
+
partiesMigrated: boolean;
|
|
6918
|
+
status: string;
|
|
6919
|
+
effectiveFrom: Date;
|
|
6920
|
+
effectiveUntil: Date | null;
|
|
6921
|
+
originalOfferId: string | null;
|
|
6922
|
+
originalPlanVersionId: string | null;
|
|
6923
|
+
originalBundleVersionIds: unknown;
|
|
6924
|
+
entitlementSnapshot: unknown;
|
|
6925
|
+
priceSnapshot: unknown;
|
|
6926
|
+
promotionSnapshots: unknown;
|
|
6927
|
+
promoCodeSnapshots: unknown;
|
|
6928
|
+
termsSnapshot: unknown;
|
|
6929
|
+
createdAt: Date;
|
|
6930
|
+
updatedAt: Date;
|
|
6931
|
+
}
|
|
6932
|
+
/** A `contract_line_items` row as either adapter reads it back. */
|
|
6933
|
+
interface CanonicalContractLineItemRow {
|
|
6934
|
+
id: string;
|
|
6935
|
+
contractId: string;
|
|
6936
|
+
kind: string;
|
|
6937
|
+
sourceKey: string;
|
|
6938
|
+
sourceVersionId: string | null;
|
|
6939
|
+
titleSnapshot: string;
|
|
6940
|
+
descriptionSnapshot: string | null;
|
|
6941
|
+
quantity: number;
|
|
6942
|
+
unit: string | null;
|
|
6943
|
+
priceNet: unknown;
|
|
6944
|
+
priceGross: unknown;
|
|
6945
|
+
billingCycle: string;
|
|
6946
|
+
currency: string;
|
|
6947
|
+
taxRate: unknown;
|
|
6948
|
+
taxAmount: unknown;
|
|
6949
|
+
minimumTermUntil: Date | null;
|
|
6950
|
+
featuresSnapshot: unknown;
|
|
6951
|
+
quotaEffectsSnapshot: unknown;
|
|
6952
|
+
metadata: unknown;
|
|
6953
|
+
createdAt: Date;
|
|
6954
|
+
}
|
|
6955
|
+
declare function toSubscriptionContractRecord(row: CanonicalContractRow, lineItems: CanonicalContractLineItemRow[]): SubscriptionContractRecord;
|
|
6956
|
+
declare function toContractLineItemRecord(row: CanonicalContractLineItemRow): ContractLineItemRecord;
|
|
6957
|
+
/**
|
|
6958
|
+
* The columns the issuer check reads off a running contract. A narrow row of
|
|
6959
|
+
* its own, because that query selects four columns rather than the whole
|
|
6960
|
+
* contract with its lines: it runs at every start, and an installation with
|
|
6961
|
+
* thousands of running contracts should not load them to count them.
|
|
6962
|
+
*/
|
|
6963
|
+
interface CanonicalRunningContractRow {
|
|
6964
|
+
id: string;
|
|
6965
|
+
tenantId: string;
|
|
6966
|
+
issuerSnapshot: unknown;
|
|
6967
|
+
effectiveFrom: Date;
|
|
6968
|
+
}
|
|
6969
|
+
/** One running contract, and the legal name on its issuer copy where it has one. */
|
|
6970
|
+
declare function toRunningContractIssuer(row: CanonicalRunningContractRow): RunningContractIssuer;
|
|
6971
|
+
|
|
4769
6972
|
/** Why a version is editable (for UI badges + audit logs). */
|
|
4770
6973
|
type VersionEditableReason = 'draft' | 'pre-active';
|
|
4771
6974
|
interface VersionEditability {
|
|
@@ -4780,6 +6983,30 @@ interface VersionEditability {
|
|
|
4780
6983
|
*/
|
|
4781
6984
|
declare function isVersionEditable(v: VersionedEntityBase, now?: Date): VersionEditability;
|
|
4782
6985
|
|
|
6986
|
+
/** The part of a plan card this rule reads and writes. */
|
|
6987
|
+
interface RecommendablePlan {
|
|
6988
|
+
planKey: string;
|
|
6989
|
+
highlight: boolean;
|
|
6990
|
+
}
|
|
6991
|
+
/**
|
|
6992
|
+
* Leaves the mark on at most one plan, in place, and returns the winner.
|
|
6993
|
+
*
|
|
6994
|
+
* `inRequestedLocale` holds the keys of the plans whose card was described in
|
|
6995
|
+
* the language that was asked for. Where a caller has no fallback to model —
|
|
6996
|
+
* the SuperAdmin edits one language at a time — passing every key, or none,
|
|
6997
|
+
* gives the same answer: the first plan in the list order wins.
|
|
6998
|
+
*
|
|
6999
|
+
* The list order is the caller's, and it is what the reader sees, so the
|
|
7000
|
+
* answer is the first recommended card on the page. Said exactly, because it
|
|
7001
|
+
* is easy to overstate: the tie-break inherits whatever order the caller
|
|
7002
|
+
* arranged, and where two plans are equal by every criterion it sorted on,
|
|
7003
|
+
* that order is the repository's. `PlanRepository.list` promises none, so an
|
|
7004
|
+
* adapter that returns rows in a different order each time would move the mark
|
|
7005
|
+
* between two otherwise indistinguishable plans. The shipped adapters order
|
|
7006
|
+
* totally.
|
|
7007
|
+
*/
|
|
7008
|
+
declare function keepOneRecommended<T extends RecommendablePlan>(plans: T[], inRequestedLocale: ReadonlySet<string>): T | null;
|
|
7009
|
+
|
|
4783
7010
|
declare const ERROR_MESSAGES_EN: Record<PlatformErrorCode, string>;
|
|
4784
7011
|
/** Values available for interpolation into a message template. */
|
|
4785
7012
|
type ErrorMessageParams = Record<string, unknown>;
|
|
@@ -4789,6 +7016,23 @@ type ErrorMessageParams = Record<string, unknown>;
|
|
|
4789
7016
|
* vanishing.
|
|
4790
7017
|
*/
|
|
4791
7018
|
declare function formatErrorMessage(template: string, params?: ErrorMessageParams): string;
|
|
7019
|
+
/**
|
|
7020
|
+
* An error body this function can read.
|
|
7021
|
+
*
|
|
7022
|
+
* `code` is widened past `PlatformErrorCode` on purpose. The function checks
|
|
7023
|
+
* `typeof body.code === 'string'` and resolves whatever it finds, and the
|
|
7024
|
+
* `overrides` parameter exists so a consumer can bring its own codes — one
|
|
7025
|
+
* consumer carries 98 of them against the platform's 135, overlapping in five.
|
|
7026
|
+
* A closed union here would reject exactly the case the parameter is for, and
|
|
7027
|
+
* the cast that works around it is one a reader has to be told is deliberate.
|
|
7028
|
+
*
|
|
7029
|
+
* `PlatformErrorBody` stays closed: a body the *platform* produces really does
|
|
7030
|
+
* carry a platform code. It is the reader that has to accept more. The same
|
|
7031
|
+
* shape appears in `SaLocale` next door, for the same reason.
|
|
7032
|
+
*/
|
|
7033
|
+
type ResolvableErrorBody = Omit<Partial<PlatformErrorBody>, 'code'> & Record<string, unknown> & {
|
|
7034
|
+
code?: PlatformErrorCode | (string & {});
|
|
7035
|
+
};
|
|
4792
7036
|
/**
|
|
4793
7037
|
* Turns an error body into display text.
|
|
4794
7038
|
*
|
|
@@ -4802,8 +7046,8 @@ declare function formatErrorMessage(template: string, params?: ErrorMessageParam
|
|
|
4802
7046
|
* second, so a template may name either without the value being duplicated on
|
|
4803
7047
|
* the wire.
|
|
4804
7048
|
*/
|
|
4805
|
-
declare function resolveErrorMessage(body:
|
|
7049
|
+
declare function resolveErrorMessage(body: ResolvableErrorBody, overrides?: Partial<Record<string, string>>, defaults?: Partial<Record<string, string>>): string;
|
|
4806
7050
|
|
|
4807
7051
|
declare const ERROR_MESSAGES_DE: Record<PlatformErrorCode, string>;
|
|
4808
7052
|
|
|
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 };
|
|
7053
|
+
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 CanonicalRunningContractRow, type CanonicalSubscriberCorrectionRow, type CanonicalSubscriberPaymentMethodRow, type CanonicalSubscriberRow, 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 CheckoutOfferSelection, type CheckoutOfferSelectionUpdate, type CheckoutOfferStatus, type CleanupResult, type CliUserRow, type ComponentKey, type ConfiguratorCatalog, type ConfiguratorMarketingProvider, type ConfiguratorModel, type ConfiguratorPlanMarketing, type ConfiguratorPlanVersionRow, type ConfiguratorPriceBreakdown, type ConfiguratorSourcesLookup, type ConfirmedPaymentMethod, type ContractErrorCode, type ContractIssuerParty, type ContractLineItemKind, type ContractLineItemRecord, type ContractSubscriberParty, type CreateBundleData, type CreateBundleVersionDraftData, type CreateCheckoutOfferData, type CreateMarketingProjectionData, type CreatePlanData, type CreatePlanVersionDraftData, type CreatePromoCodeData, type CreatePromoCodeRequest, type CreatePromotionData, type CreateSubscriberData, 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, ForeignPaymentMethodReferenceError, type ImmediatePlanChangeInput, type InvoiceLineItemSnapshot, type IssuerCorrectionFault, type IssuerIdentityChange, type KpiCardDef, type KpiDisplayHint, LEGAL_IDENTITY_FIELDS, type LegalIdentity, type LegalIdentityField, 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 MaskedPaymentMethod, type MfaPort, type NewContractLineItemData, type NewSettingsChange, type NewSubscriberDetails, type NewSubscriptionContractData, 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, PAYMENT_ERROR_CODES, PAYMENT_METHOD_TYPES, PENDING_CHECKOUT_TTL_DAYS, PENDING_EMAIL_TTL_HOURS, PENDING_ONBOARDING_TTL_DAYS, PLATFORM_ERROR_CODES, PROMO_ERROR_CODES, type Paginated, type PartyAddress, type PasswordHasher, type PasswordResetCliResult, PaymentCallbackRejectedError, type PaymentErrorCode, type PaymentEventClaim, type PaymentEventLog, type PaymentGateway, type PaymentGatewayCallback, type PaymentGatewayEvent, type PaymentMethodHolder, type PaymentMethodSetupSession, type PaymentMethodSetupSubject, type PaymentMethodType, 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 PlanCatalogIssuer, type PlanCatalogIssuerCorrection, type PlanCatalogLookup, type PlanCatalogMarketing, type PlanCatalogNotifications, type PlanCatalogPaymentAccount, type PlanCatalogPayments, type PlanCatalogReadSink, type PlanCatalogReadSnapshot, type PlanCatalogSettings, type PlanCatalogSubscribers, 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 PromoCodeHoldRecord, type PromoCodeHoldRepository, type PromoCodeHoldTaken, 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 PromotionOnPrice, 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 PublicNewPaymentMethods, 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 RecordSubscriberPaymentMethodData, type RecordSubscriberPaymentMethodOutcome, type RecordSubscriberPaymentMethodResult, type RedeemPromoInTransactionCallback, type RegistrationActivation, type RegistrationAuditContext, type RegistrationAuditEvent, type RegistrationAuditEventType, type RegistrationAuditLogger, type RegistrationBillingDetails, 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, type RunningContractIssuer, type RunningContractIssuers, SETTINGS_ERROR_CODES, SETUP_ERROR_CODES, SUBSCRIBER_ERROR_CODES, type SaaSiCatPersistenceAdapter, type SaaSiCatPersistenceAdminResources, type SaaSiCatPersistenceCatalog, type SaaSiCatPersistenceCore, type SaaSiCatPersistenceEntitlement, type SaaSiCatPersistencePayments, 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 StartPaymentMethodSetupInput, type StartRegistrationInput, type StartRegistrationResult, type StoredBundleStem, type StrictModeWarning, type StrictModeWarningCode, type SubscriberContact, type SubscriberContactChange, type SubscriberCorrectionData, type SubscriberCorrectionRecord, type SubscriberCorrectionResult, type SubscriberDetails, type SubscriberErrorCode, type SubscriberIdentityCorrection, type SubscriberIdentityDelta, type SubscriberIdentityValues, type SubscriberPaymentMethodRecord, type SubscriberPaymentMethodReference, type SubscriberPaymentMethodRepository, type SubscriberPaymentMethodSetupData, type SubscriberPaymentMethodSetupMatch, type SubscriberPaymentMethodStatus, type SubscriberRecord, type SubscriberRepository, type Subscription, type SubscriptionBundleRecord, type SubscriptionBundleRepository, type SubscriptionBundleView, type SubscriptionContractFilter, type SubscriptionContractInvoiceSnapshot, type SubscriptionContractParties, 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, classifyIssuerChange, classifyPlanDiff, collectUnsatisfiedRequires, contractPartiesOf, coverageExcludingSelf, definedFields, diffSettings, formatCustomerNumber, formatErrorMessage, identityCorrectionDelta, isBundleRedundant, isForeignPaymentMethodReferenceError, isPaymentCallbackRejectedError, isPlatformUserExistsError, isVersionEditable, issuerIdentityOf, keepOneRecommended, missingRequiresFor, movedIdentityFields, pickActivePromo, planCatalogSettingsOf, previousUtcDay, promoStatus, promotionOnPrice, readQuotaRecord, readQuotaValue, recordedIssuerIdentity, refuseForeignPaymentMethodReference, resolveBundleAvailability, resolveErrorMessage, sameLegalIdentity, selectChargeableBundles, settingsSubtreeOf, startOfUtcDay, subscriberFromRegistration, subscriberPaymentMethodColumns, toBundleStemRow, toContractLineItemRecord, toPlanRow, toPlanVersionRow, toRunningContractIssuer, toSubscriberCorrectionRecord, toSubscriberPaymentMethodRecord, toSubscriberRecord, toSubscriptionContractRecord };
|