@saasicat/spec 0.2.0
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/LICENSE +202 -0
- package/README.md +59 -0
- package/acceptance/README.md +62 -0
- package/acceptance/manifest/full-manifest-requires-super-admin.yaml +43 -0
- package/acceptance/manifest/public-boot-no-auth.yaml +40 -0
- package/acceptance/mfa/totp-verify-good-and-bad-code.yaml +57 -0
- package/acceptance/plan-version/publish-does-not-touch-bestand.yaml +62 -0
- package/acceptance/promo/first-time-only-blocks-second-redemption.yaml +40 -0
- package/acceptance/tenant/suspend-creates-audit-and-blocks-login.yaml +69 -0
- package/admin-api.openapi.yaml +1724 -0
- package/cli-conventions.md +158 -0
- package/index.cjs +18 -0
- package/index.d.cts +13 -0
- package/index.d.ts +14 -0
- package/index.js +17 -0
- package/package.json +63 -0
- package/prisma-fragments/01-subscription.prisma +216 -0
- package/prisma-fragments/02-promo-code.prisma +145 -0
- package/prisma-fragments/03-plan-versions.prisma +94 -0
- package/prisma-fragments/04-audit-log.prisma +38 -0
- package/prisma-fragments/05-bundle-business-type.prisma +206 -0
- package/prisma-fragments/06-catalog-entries.prisma +279 -0
- package/prisma-fragments/07-promotion.prisma +65 -0
- package/prisma-fragments/08-subscription-contract.prisma +92 -0
- package/prisma-fragments/09-pending-registration.prisma +96 -0
- package/prisma-fragments/README.md +115 -0
- package/schemas/admin-manifest.schema.json +328 -0
- package/schemas/audit-event.schema.json +73 -0
- package/schemas/plan-catalog.schema.json +166 -0
- package/schemas/promo-code.schema.json +214 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// SaaS platform Prisma fragment: SubscriptionContract + ContractLineItem
|
|
3
|
+
// =============================================================================
|
|
4
|
+
//
|
|
5
|
+
// REFERENCE SNIPPET — not a standalone schema. Consumers copy the models into
|
|
6
|
+
// their `schema.prisma` and add FK relations to Tenant/User.
|
|
7
|
+
//
|
|
8
|
+
// V3 rule: billing and entitlement must not depend on live-mutable catalog
|
|
9
|
+
// tables. `SubscriptionContract` and `ContractLineItem` therefore store full
|
|
10
|
+
// snapshots. Catalog FKs are optional and only audit/trace references.
|
|
11
|
+
|
|
12
|
+
enum SubscriptionContractStatus {
|
|
13
|
+
active
|
|
14
|
+
scheduled
|
|
15
|
+
terminated
|
|
16
|
+
superseded
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
enum ContractLineItemKind {
|
|
20
|
+
plan
|
|
21
|
+
bundle
|
|
22
|
+
quota
|
|
23
|
+
feature
|
|
24
|
+
discount
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
model SubscriptionContract {
|
|
28
|
+
id String @id @default(uuid())
|
|
29
|
+
projectKey String
|
|
30
|
+
tenantId String
|
|
31
|
+
|
|
32
|
+
status SubscriptionContractStatus @default(active)
|
|
33
|
+
effectiveFrom DateTime
|
|
34
|
+
effectiveUntil DateTime?
|
|
35
|
+
|
|
36
|
+
// Optional trace back to the offer/catalog. The contract stays valid even
|
|
37
|
+
// when these target objects are archived or physically deleted.
|
|
38
|
+
originalOfferId String?
|
|
39
|
+
originalPlanVersionId String?
|
|
40
|
+
originalBundleVersionIds Json @default("[]")
|
|
41
|
+
|
|
42
|
+
// Full snapshots for runtime, billing, and later display.
|
|
43
|
+
entitlementSnapshot Json?
|
|
44
|
+
priceSnapshot Json
|
|
45
|
+
promotionSnapshots Json @default("[]")
|
|
46
|
+
promoCodeSnapshots Json @default("[]")
|
|
47
|
+
termsSnapshot Json?
|
|
48
|
+
|
|
49
|
+
lineItems ContractLineItem[]
|
|
50
|
+
|
|
51
|
+
createdAt DateTime @default(now())
|
|
52
|
+
updatedAt DateTime @updatedAt
|
|
53
|
+
|
|
54
|
+
// tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
|
|
55
|
+
|
|
56
|
+
@@index([tenantId, status, effectiveFrom])
|
|
57
|
+
@@index([projectKey, status])
|
|
58
|
+
@@index([originalOfferId])
|
|
59
|
+
@@map("subscription_contracts")
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
model ContractLineItem {
|
|
63
|
+
id String @id @default(uuid())
|
|
64
|
+
contractId String
|
|
65
|
+
|
|
66
|
+
kind ContractLineItemKind
|
|
67
|
+
sourceKey String
|
|
68
|
+
sourceVersionId String?
|
|
69
|
+
|
|
70
|
+
titleSnapshot String
|
|
71
|
+
descriptionSnapshot String?
|
|
72
|
+
|
|
73
|
+
quantity Int @default(1)
|
|
74
|
+
unit String?
|
|
75
|
+
priceNet Decimal @db.Decimal(10, 2)
|
|
76
|
+
priceGross Decimal @db.Decimal(10, 2)
|
|
77
|
+
billingCycle String // monthly | yearly
|
|
78
|
+
|
|
79
|
+
minimumTermUntil DateTime?
|
|
80
|
+
|
|
81
|
+
featuresSnapshot Json @default("[]")
|
|
82
|
+
quotaEffectsSnapshot Json @default("{}")
|
|
83
|
+
metadata Json?
|
|
84
|
+
|
|
85
|
+
createdAt DateTime @default(now())
|
|
86
|
+
|
|
87
|
+
contract SubscriptionContract @relation(fields: [contractId], references: [id], onDelete: Cascade)
|
|
88
|
+
|
|
89
|
+
@@index([contractId, kind])
|
|
90
|
+
@@index([sourceVersionId])
|
|
91
|
+
@@map("contract_line_items")
|
|
92
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// SaaS Platform Prisma fragment: PendingRegistration + PaymentEventLog
|
|
3
|
+
// =============================================================================
|
|
4
|
+
//
|
|
5
|
+
// REFERENCE SNIPPET — see 01-subscription.prisma for conventions.
|
|
6
|
+
//
|
|
7
|
+
// Persistence of the multi-step registration and onboarding flow
|
|
8
|
+
// (`PendingRegistrationService`). A PendingRegistration holds the
|
|
9
|
+
// intermediate state between step 1 (capturing sign-up data) and the final
|
|
10
|
+
// activation (step 4: payment). Only after successful payment does it become
|
|
11
|
+
// User + Tenant + Subscription — until then the record deliberately stays
|
|
12
|
+
// without a foreign key to consumer models.
|
|
13
|
+
//
|
|
14
|
+
// Unlike the other fragments, WITHOUT `@@map`: the model was adopted back from
|
|
15
|
+
// the reference implementation, whose tables already live under the Prisma
|
|
16
|
+
// default names — a subsequent `@@map` would only force a table rename.
|
|
17
|
+
//
|
|
18
|
+
// Contract: saas-platform-types/registration.types.ts (PendingRegistration,
|
|
19
|
+
// PendingRegistrationRepository, PaymentEventLog).
|
|
20
|
+
// Service logic: saas-platform-nest/registration/PendingRegistrationService.
|
|
21
|
+
|
|
22
|
+
// Multi-step registration flow: status of a PendingRegistration
|
|
23
|
+
// between sign-up-data capture (step 1) and final activation
|
|
24
|
+
// after payment (step 4).
|
|
25
|
+
enum RegistrationStatus {
|
|
26
|
+
PENDING_EMAIL_VERIFICATION
|
|
27
|
+
EMAIL_VERIFIED
|
|
28
|
+
PLAN_SELECTED
|
|
29
|
+
CHECKOUT_STARTED
|
|
30
|
+
EXPIRED
|
|
31
|
+
DELETED
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
model PendingRegistration {
|
|
35
|
+
id String @id @default(cuid())
|
|
36
|
+
|
|
37
|
+
tenantName String
|
|
38
|
+
tenantSlug String?
|
|
39
|
+
salutation String?
|
|
40
|
+
firstName String
|
|
41
|
+
lastName String
|
|
42
|
+
email String @unique
|
|
43
|
+
passwordHash String
|
|
44
|
+
locale String @default("de")
|
|
45
|
+
|
|
46
|
+
status RegistrationStatus @default(PENDING_EMAIL_VERIFICATION)
|
|
47
|
+
currentStep Int @default(2)
|
|
48
|
+
|
|
49
|
+
emailVerifiedAt DateTime?
|
|
50
|
+
|
|
51
|
+
otpHash String?
|
|
52
|
+
otpExpiresAt DateTime?
|
|
53
|
+
otpSendCount Int @default(1)
|
|
54
|
+
lastOtpSentAt DateTime?
|
|
55
|
+
// Persistent failed-attempt counter of the OTP verification. From
|
|
56
|
+
// OTP_VERIFY_MAX_ATTEMPTS on, verifyOtp() throws OTP_LOCKED — even with
|
|
57
|
+
// a correct code afterwards. A newly generated OTP resets it to 0
|
|
58
|
+
// (sending is rate-limited separately via otpSendCount).
|
|
59
|
+
otpAttemptCount Int @default(0)
|
|
60
|
+
|
|
61
|
+
selectedPlanId String?
|
|
62
|
+
// Configurator selection (Step 3):
|
|
63
|
+
// - configJson: JSON snapshot of the RegistrationConfigSelection.
|
|
64
|
+
// - billingCycle: 'MONTHLY' | 'YEARLY' — determines the price calculation.
|
|
65
|
+
// - appliedPromoCode: last-applied code (plaintext, only for UI display;
|
|
66
|
+
// validation runs fresh every time via PromoCodesService).
|
|
67
|
+
configJson Json?
|
|
68
|
+
billingCycle String?
|
|
69
|
+
appliedPromoCode String?
|
|
70
|
+
checkoutSessionId String?
|
|
71
|
+
checkoutStartedAt DateTime?
|
|
72
|
+
|
|
73
|
+
expiresAt DateTime
|
|
74
|
+
createdAt DateTime @default(now())
|
|
75
|
+
updatedAt DateTime @updatedAt
|
|
76
|
+
|
|
77
|
+
@@index([status, expiresAt])
|
|
78
|
+
@@index([tenantSlug])
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// PaymentEventLog — idempotency key for payment webhooks.
|
|
82
|
+
// Prevents a doubly delivered webhook (Stripe at-least-once) from triggering
|
|
83
|
+
// the final activation more than once. `eventId` is the provider event ID
|
|
84
|
+
// (Stripe `event.id`), `tryClaim` uses a @unique INSERT as race protection.
|
|
85
|
+
model PaymentEventLog {
|
|
86
|
+
id String @id @default(cuid())
|
|
87
|
+
eventId String @unique
|
|
88
|
+
provider String
|
|
89
|
+
sessionId String?
|
|
90
|
+
status String
|
|
91
|
+
payload Json?
|
|
92
|
+
processedAt DateTime @default(now())
|
|
93
|
+
|
|
94
|
+
@@index([sessionId])
|
|
95
|
+
@@index([status, processedAt])
|
|
96
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
---
|
|
2
|
+
doc_title: SaaS Platform Prisma Fragments
|
|
3
|
+
status: living document
|
|
4
|
+
related:
|
|
5
|
+
- ../schemas/plan-catalog.schema.json
|
|
6
|
+
- ../schemas/promo-code.schema.json
|
|
7
|
+
- ../schemas/audit-event.schema.json
|
|
8
|
+
- ../../saas-platform-types/src/subscription.types.ts
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Prisma Fragments
|
|
12
|
+
|
|
13
|
+
Reference snippets that document the **canonical database schema** of the SaaS
|
|
14
|
+
platform. Consumer apps copy the models into their own `schema.prisma` and add
|
|
15
|
+
FK relations to their project-specific `Tenant`/`User` models.
|
|
16
|
+
|
|
17
|
+
## Files
|
|
18
|
+
|
|
19
|
+
| File | Models |
|
|
20
|
+
| -------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
|
|
21
|
+
| [`01-subscription.prisma`](01-subscription.prisma) | `Subscription`, `SubscriptionPaymentMethod`, `CheckoutOffer` + Enums |
|
|
22
|
+
| [`02-promo-code.prisma`](02-promo-code.prisma) | `PromoCode`, `PromoCodeRedemption`, `PromoCodeValidationLog` + Enums |
|
|
23
|
+
| [`03-plan-versions.prisma`](03-plan-versions.prisma) | `Plan`, `PlanVersion` |
|
|
24
|
+
| [`04-audit-log.prisma`](04-audit-log.prisma) | `AuditLog` |
|
|
25
|
+
| [`05-bundle-business-type.prisma`](05-bundle-business-type.prisma) | `Bundle`, `BundleVersion`, `BusinessType`, `BusinessTypeVersion`, `BusinessTypeBundle` |
|
|
26
|
+
| [`06-catalog-entries.prisma`](06-catalog-entries.prisma) | `CapabilityCatalogEntry`, `FeatureCatalogEntry`, `MarketingProjection` |
|
|
27
|
+
| [`07-promotion.prisma`](07-promotion.prisma) | `Promotion` |
|
|
28
|
+
| [`08-subscription-contract.prisma`](08-subscription-contract.prisma) | `SubscriptionContract`, `ContractLineItem` |
|
|
29
|
+
| [`09-pending-registration.prisma`](09-pending-registration.prisma) | `PendingRegistration`, `PaymentEventLog` + `RegistrationStatus` |
|
|
30
|
+
|
|
31
|
+
## How the consumer uses the fragments
|
|
32
|
+
|
|
33
|
+
Prisma does **not** support real schema merging — there is no include mechanism.
|
|
34
|
+
Consumers must add the models into their own `schema.prisma`. There are two
|
|
35
|
+
pragmatic approaches:
|
|
36
|
+
|
|
37
|
+
### Variant A — Copy-Paste (recommended)
|
|
38
|
+
|
|
39
|
+
1. Add the required models from the fragments into your own `schema.prisma`.
|
|
40
|
+
2. Enable the commented-out FK relations to consumer models (`Tenant`, `User`)
|
|
41
|
+
and adapt them to your own model names.
|
|
42
|
+
3. Plan/feature keys stay as `String` — the source of truth is the set of plans
|
|
43
|
+
maintained in the SuperAdmin UI (DB) and the feature/quota catalog published
|
|
44
|
+
via Discovery.
|
|
45
|
+
|
|
46
|
+
### Variant B — Schema stitching via codegen
|
|
47
|
+
|
|
48
|
+
Tools such as [`prisma-import`](https://github.com/ajmnz/prisma-import) allow
|
|
49
|
+
include directives; they generate a merged `schema.prisma`. For today's
|
|
50
|
+
single-repo consumers, Variant A is simpler.
|
|
51
|
+
|
|
52
|
+
## Conventions
|
|
53
|
+
|
|
54
|
+
### 1. Keys are strings, not enums
|
|
55
|
+
|
|
56
|
+
`plan` (`Subscription.plan`, `Subscription.pendingPlan`, …) and
|
|
57
|
+
`featureKey` are declared as `String`. The source of truth is the plan master
|
|
58
|
+
records (`plans` table, maintained in the SuperAdmin UI) and the feature catalog
|
|
59
|
+
(`feature_catalog_entries`).
|
|
60
|
+
|
|
61
|
+
If you prefer **Postgres enums**: declare an enum locally and cast the field
|
|
62
|
+
via `@db.<EnumName>`. Not a platform requirement — the platform services only
|
|
63
|
+
read strings.
|
|
64
|
+
|
|
65
|
+
### 2. FKs to consumer models are documented but commented out
|
|
66
|
+
|
|
67
|
+
Fields such as `tenantId String` and `userId String?` remain as plain
|
|
68
|
+
string columns in the fragments; the corresponding `@relation` is left as a
|
|
69
|
+
comment. The consumer enables them using their own `Tenant`/`User` model names.
|
|
70
|
+
|
|
71
|
+
### 3. Table names (`@@map`) are canonical
|
|
72
|
+
|
|
73
|
+
`subscriptions`, `plan_versions`, `promo_codes`, `promo_code_redemptions`,
|
|
74
|
+
`promo_code_validation_logs`, `audit_logs`, `bundles`, `bundle_versions`,
|
|
75
|
+
`business_types`, `business_type_versions`, `business_type_bundles`,
|
|
76
|
+
`capability_catalog_entries`, `feature_catalog_entries`,
|
|
77
|
+
`marketing_projections`, `subscription_contracts`, `contract_line_items`.
|
|
78
|
+
Please do **not change** them — otherwise platform migration scripts and the
|
|
79
|
+
`@saasicat/cli` commands that rely on these names will break.
|
|
80
|
+
|
|
81
|
+
### 4. Decimal precision
|
|
82
|
+
|
|
83
|
+
All monetary amounts are `Decimal(10, 2)` (max ±99,999,999.99 €), promo-code
|
|
84
|
+
values are `Decimal(8, 2)` (percentage or amount). Consumers should not relax
|
|
85
|
+
this precision.
|
|
86
|
+
|
|
87
|
+
### 5. Partial unique index for drafts
|
|
88
|
+
|
|
89
|
+
`PlanVersion`, `BundleVersion`
|
|
90
|
+
and `BusinessTypeVersion` allow **exactly one** draft per
|
|
91
|
+
identity key (`publishedAt IS NULL`). The Prisma schema cannot express this —
|
|
92
|
+
add it in the SQL migration:
|
|
93
|
+
|
|
94
|
+
```sql
|
|
95
|
+
CREATE UNIQUE INDEX plan_versions_draft_per_plan
|
|
96
|
+
ON plan_versions (plan_id) WHERE published_at IS NULL;
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Likewise for:
|
|
100
|
+
|
|
101
|
+
- `bundle_versions` (per `bundle_id`)
|
|
102
|
+
- `business_type_versions` (per `business_type_id`)
|
|
103
|
+
|
|
104
|
+
## Design decisions
|
|
105
|
+
|
|
106
|
+
- **No fixed quota columns** (`maxUsers`, `maxStorageGb`, …) — limits
|
|
107
|
+
live generically in `quotas Json`; the allowed keys are declared in code
|
|
108
|
+
via `@DefinesQuota`.
|
|
109
|
+
- **No `SubscriptionPlan`/`FeatureKey` enums** — both fields are declared
|
|
110
|
+
as `String` (see Convention 1).
|
|
111
|
+
- **No add-on tables (#49)** — `subscription_addons`,
|
|
112
|
+
`unit_addon_versions`, `feature_addon_versions` are not a
|
|
113
|
+
sales surface; only plan versions + bundles are sold.
|
|
114
|
+
- **App-specific tables** (e.g. invoice or bank master data)
|
|
115
|
+
belong in the schema of the consuming app, not in the platform.
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://saasicat.dev/schemas/admin-manifest.schema.json",
|
|
4
|
+
"title": "AdminManifest",
|
|
5
|
+
"description": "UI discovery projection of a SaaS app. Served by the app backend under GET /api/v1/admin/manifest and consumed by the shared UI shell.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": [
|
|
8
|
+
"schemaVersion",
|
|
9
|
+
"project",
|
|
10
|
+
"build",
|
|
11
|
+
"planCatalogSnapshot",
|
|
12
|
+
"capabilities",
|
|
13
|
+
"navigation"
|
|
14
|
+
],
|
|
15
|
+
"additionalProperties": false,
|
|
16
|
+
"properties": {
|
|
17
|
+
"schemaVersion": {
|
|
18
|
+
"type": "integer",
|
|
19
|
+
"const": 1
|
|
20
|
+
},
|
|
21
|
+
|
|
22
|
+
"project": {
|
|
23
|
+
"type": "object",
|
|
24
|
+
"required": ["key", "displayName"],
|
|
25
|
+
"additionalProperties": false,
|
|
26
|
+
"properties": {
|
|
27
|
+
"key": { "type": "string", "pattern": "^[a-z][a-z0-9-]{1,30}$" },
|
|
28
|
+
"displayName": { "type": "string", "minLength": 1, "maxLength": 60 },
|
|
29
|
+
"label": {
|
|
30
|
+
"type": "string",
|
|
31
|
+
"minLength": 1,
|
|
32
|
+
"maxLength": 60,
|
|
33
|
+
"description": "Tag/subtitle (e.g. \"SuperAdmin\"). From saas.yaml#app.label."
|
|
34
|
+
},
|
|
35
|
+
"icon": {
|
|
36
|
+
"type": "string",
|
|
37
|
+
"minLength": 1,
|
|
38
|
+
"maxLength": 8,
|
|
39
|
+
"description": "Short abbreviation for the logo badge (e.g. \"ma\", \"da\"). From saas.yaml#app.icon."
|
|
40
|
+
},
|
|
41
|
+
"logoUrl": { "type": "string" },
|
|
42
|
+
"environment": {
|
|
43
|
+
"type": "string",
|
|
44
|
+
"enum": ["production", "staging", "development"]
|
|
45
|
+
},
|
|
46
|
+
"availableLocales": {
|
|
47
|
+
"type": "array",
|
|
48
|
+
"items": { "type": "string", "pattern": "^[a-z]{2}(-[A-Z]{2})?$" },
|
|
49
|
+
"minItems": 1,
|
|
50
|
+
"uniqueItems": true,
|
|
51
|
+
"description": "Locales allowed by the app catalog (saas.yaml marketing.availableLocales). First = default. SPEC_V2 §6.5."
|
|
52
|
+
},
|
|
53
|
+
"defaultLocale": {
|
|
54
|
+
"type": "string",
|
|
55
|
+
"pattern": "^[a-z]{2}(-[A-Z]{2})?$",
|
|
56
|
+
"description": "Default locale; corresponds to availableLocales[0]."
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
"build": {
|
|
62
|
+
"type": "object",
|
|
63
|
+
"required": ["platformPackageVersion", "appVersion", "manifestHash"],
|
|
64
|
+
"additionalProperties": false,
|
|
65
|
+
"properties": {
|
|
66
|
+
"platformPackageVersion": { "type": "string" },
|
|
67
|
+
"appVersion": { "type": "string" },
|
|
68
|
+
"manifestHash": { "type": "string", "pattern": "^sha256-[A-Za-z0-9+/=_-]{32,}$" }
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
"planCatalogSnapshot": {
|
|
73
|
+
"type": "object",
|
|
74
|
+
"required": ["source", "hash", "currency", "vatRate", "plans"],
|
|
75
|
+
"additionalProperties": false,
|
|
76
|
+
"properties": {
|
|
77
|
+
"source": {
|
|
78
|
+
"type": "string",
|
|
79
|
+
"description": "Path to the source, e.g. 'config/saas.yaml'"
|
|
80
|
+
},
|
|
81
|
+
"hash": { "type": "string", "pattern": "^sha256-[A-Za-z0-9+/=_-]{32,}$" },
|
|
82
|
+
"currency": { "type": "string", "pattern": "^[A-Z]{3}$" },
|
|
83
|
+
"vatRate": { "type": "number", "minimum": 0, "maximum": 100 },
|
|
84
|
+
"features": {
|
|
85
|
+
"type": "array",
|
|
86
|
+
"items": {
|
|
87
|
+
"type": "object",
|
|
88
|
+
"required": ["key"],
|
|
89
|
+
"additionalProperties": false,
|
|
90
|
+
"properties": {
|
|
91
|
+
"key": { "type": "string", "pattern": "^[A-Z][A-Z0-9_]+$" },
|
|
92
|
+
"label": { "type": "string" },
|
|
93
|
+
"tier": { "type": "string" }
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
"plans": {
|
|
98
|
+
"type": "array",
|
|
99
|
+
"items": { "$ref": "#/$defs/PlanDef" },
|
|
100
|
+
"minItems": 1
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
|
|
105
|
+
"capabilities": {
|
|
106
|
+
"type": "object",
|
|
107
|
+
"description": "Backend capabilities. Naming convention: domain.action or domain.resource.action. Values are boolean (true = active in the build).",
|
|
108
|
+
"patternProperties": {
|
|
109
|
+
"^[a-z][a-zA-Z0-9]*\\.[a-z][a-zA-Z0-9]*(?:\\.[a-z][a-zA-Z0-9]*)?$": {
|
|
110
|
+
"type": "boolean"
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
"additionalProperties": false
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
"navigation": {
|
|
117
|
+
"type": "object",
|
|
118
|
+
"required": ["standardPages"],
|
|
119
|
+
"additionalProperties": false,
|
|
120
|
+
"properties": {
|
|
121
|
+
"standardPages": {
|
|
122
|
+
"type": "object",
|
|
123
|
+
"additionalProperties": false,
|
|
124
|
+
"properties": {
|
|
125
|
+
"dashboard": { "$ref": "#/$defs/StandardPageDef" },
|
|
126
|
+
"tenants": { "$ref": "#/$defs/StandardPageDef" },
|
|
127
|
+
"subscriptions": { "$ref": "#/$defs/StandardPageDef" },
|
|
128
|
+
"promoCodes": { "$ref": "#/$defs/StandardPageDef" },
|
|
129
|
+
"plans": { "$ref": "#/$defs/StandardPageDef" },
|
|
130
|
+
"planVersions": { "$ref": "#/$defs/StandardPageDef" },
|
|
131
|
+
"audit": { "$ref": "#/$defs/StandardPageDef" },
|
|
132
|
+
"users": { "$ref": "#/$defs/StandardPageDef" },
|
|
133
|
+
"pilots": { "$ref": "#/$defs/StandardPageDef" },
|
|
134
|
+
"discovery": { "$ref": "#/$defs/StandardPageDef" },
|
|
135
|
+
"bundles": { "$ref": "#/$defs/StandardPageDef" },
|
|
136
|
+
"businessTypes": { "$ref": "#/$defs/StandardPageDef" },
|
|
137
|
+
"marketingCatalog": { "$ref": "#/$defs/StandardPageDef" },
|
|
138
|
+
"platformEmail": { "$ref": "#/$defs/StandardPageDef" },
|
|
139
|
+
"platformEmailHistory": { "$ref": "#/$defs/StandardPageDef" }
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
"projectPages": {
|
|
143
|
+
"type": "array",
|
|
144
|
+
"items": { "$ref": "#/$defs/ProjectPageDef" }
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
|
|
149
|
+
"dashboard": {
|
|
150
|
+
"type": "object",
|
|
151
|
+
"additionalProperties": false,
|
|
152
|
+
"properties": {
|
|
153
|
+
"kpiCards": {
|
|
154
|
+
"type": "array",
|
|
155
|
+
"items": { "$ref": "#/$defs/KpiCardDef" }
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
|
|
160
|
+
"tenants": {
|
|
161
|
+
"type": "object",
|
|
162
|
+
"additionalProperties": false,
|
|
163
|
+
"properties": {
|
|
164
|
+
"columns": {
|
|
165
|
+
"type": "array",
|
|
166
|
+
"items": { "$ref": "#/$defs/TenantColumnDef" }
|
|
167
|
+
},
|
|
168
|
+
"actions": {
|
|
169
|
+
"type": "array",
|
|
170
|
+
"items": { "$ref": "#/$defs/TenantActionDef" }
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
|
|
175
|
+
"audit": {
|
|
176
|
+
"type": "object",
|
|
177
|
+
"additionalProperties": false,
|
|
178
|
+
"properties": {
|
|
179
|
+
"actions": {
|
|
180
|
+
"type": "array",
|
|
181
|
+
"items": {
|
|
182
|
+
"type": "object",
|
|
183
|
+
"required": ["key", "label"],
|
|
184
|
+
"additionalProperties": false,
|
|
185
|
+
"properties": {
|
|
186
|
+
"key": { "type": "string", "pattern": "^[A-Z][A-Z0-9_]+$" },
|
|
187
|
+
"label": { "type": "string" },
|
|
188
|
+
"severity": {
|
|
189
|
+
"type": "string",
|
|
190
|
+
"enum": ["info", "low", "medium", "high"]
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
|
|
199
|
+
"$defs": {
|
|
200
|
+
"CapabilityKey": {
|
|
201
|
+
"type": "string",
|
|
202
|
+
"pattern": "^[a-z][a-zA-Z0-9]*\\.[a-z][a-zA-Z0-9]*(?:\\.[a-z][a-zA-Z0-9]*)?$"
|
|
203
|
+
},
|
|
204
|
+
|
|
205
|
+
"StandardPageDef": {
|
|
206
|
+
"type": "object",
|
|
207
|
+
"required": ["enabled"],
|
|
208
|
+
"additionalProperties": false,
|
|
209
|
+
"properties": {
|
|
210
|
+
"enabled": { "type": "boolean" },
|
|
211
|
+
"requiredCapability": { "$ref": "#/$defs/CapabilityKey" }
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
|
|
215
|
+
"ProjectPageDef": {
|
|
216
|
+
"type": "object",
|
|
217
|
+
"required": ["id", "label", "route", "componentKey"],
|
|
218
|
+
"additionalProperties": false,
|
|
219
|
+
"properties": {
|
|
220
|
+
"id": { "type": "string", "pattern": "^[a-z][a-z0-9-]+\\.[a-z][a-zA-Z0-9-]*$" },
|
|
221
|
+
"label": { "type": "string", "minLength": 1 },
|
|
222
|
+
"icon": { "type": "string" },
|
|
223
|
+
"route": { "type": "string", "pattern": "^/[A-Za-z0-9-/]+$" },
|
|
224
|
+
"navSection": { "type": "string" },
|
|
225
|
+
"componentKey": {
|
|
226
|
+
"type": "string",
|
|
227
|
+
"pattern": "^[a-z][a-z0-9-]+\\.[a-z][a-zA-Z0-9-]*$"
|
|
228
|
+
},
|
|
229
|
+
"requiredCapability": { "$ref": "#/$defs/CapabilityKey" },
|
|
230
|
+
"prefetchOnIdle": { "type": "boolean", "default": false }
|
|
231
|
+
}
|
|
232
|
+
},
|
|
233
|
+
|
|
234
|
+
"KpiCardDef": {
|
|
235
|
+
"type": "object",
|
|
236
|
+
"required": ["id", "label", "endpoint", "displayHint"],
|
|
237
|
+
"additionalProperties": false,
|
|
238
|
+
"properties": {
|
|
239
|
+
"id": { "type": "string", "pattern": "^[a-z][a-z0-9-]+\\.[a-z][a-zA-Z0-9.-]*$" },
|
|
240
|
+
"label": { "type": "string", "minLength": 1 },
|
|
241
|
+
"endpoint": {
|
|
242
|
+
"type": "string",
|
|
243
|
+
"pattern": "^/api(/v[0-9]+)?/admin/(extras|dashboard)/"
|
|
244
|
+
},
|
|
245
|
+
"displayHint": {
|
|
246
|
+
"type": "object",
|
|
247
|
+
"required": ["type"],
|
|
248
|
+
"additionalProperties": false,
|
|
249
|
+
"properties": {
|
|
250
|
+
"type": {
|
|
251
|
+
"type": "string",
|
|
252
|
+
"enum": ["value", "value+timestamp", "value+spark8w", "value+delta"]
|
|
253
|
+
},
|
|
254
|
+
"icon": { "type": "string" }
|
|
255
|
+
}
|
|
256
|
+
},
|
|
257
|
+
"slotPriority": { "type": "integer", "minimum": 0, "maximum": 100 },
|
|
258
|
+
"requiredCapability": { "$ref": "#/$defs/CapabilityKey" }
|
|
259
|
+
}
|
|
260
|
+
},
|
|
261
|
+
|
|
262
|
+
"TenantColumnDef": {
|
|
263
|
+
"type": "object",
|
|
264
|
+
"required": ["key", "label", "endpoint"],
|
|
265
|
+
"additionalProperties": false,
|
|
266
|
+
"properties": {
|
|
267
|
+
"key": { "type": "string", "pattern": "^[a-z][a-zA-Z0-9]*$" },
|
|
268
|
+
"label": { "type": "string", "minLength": 1 },
|
|
269
|
+
"endpoint": {
|
|
270
|
+
"type": "string",
|
|
271
|
+
"pattern": "^/api(/v[0-9]+)?/admin/extras/[A-Za-z0-9/-]+$",
|
|
272
|
+
"not": { "pattern": "\\{slug\\}|\\{tenantId\\}" },
|
|
273
|
+
"description": "Required: batch-capable endpoint, no tenant slug in the path. UI calls it with ?tenantIds=... See SPEC §4.4.1."
|
|
274
|
+
},
|
|
275
|
+
"requiredCapability": { "$ref": "#/$defs/CapabilityKey" }
|
|
276
|
+
}
|
|
277
|
+
},
|
|
278
|
+
|
|
279
|
+
"TenantActionDef": {
|
|
280
|
+
"type": "object",
|
|
281
|
+
"required": ["id", "label", "actionKey"],
|
|
282
|
+
"additionalProperties": false,
|
|
283
|
+
"properties": {
|
|
284
|
+
"id": {
|
|
285
|
+
"type": "string",
|
|
286
|
+
"pattern": "^[a-z][a-z0-9-]+(?:\\.[a-z][a-zA-Z0-9-]*)+$"
|
|
287
|
+
},
|
|
288
|
+
"label": { "type": "string", "minLength": 1 },
|
|
289
|
+
"actionKey": {
|
|
290
|
+
"$ref": "#/$defs/CapabilityKey",
|
|
291
|
+
"description": "Lookup in the frontend action registry. NO endpoint/method here."
|
|
292
|
+
},
|
|
293
|
+
"requiredCapability": { "$ref": "#/$defs/CapabilityKey" },
|
|
294
|
+
"requiresMfa": { "type": "boolean" },
|
|
295
|
+
"confirmType": {
|
|
296
|
+
"type": "string",
|
|
297
|
+
"enum": ["none", "simple", "typed-slug", "typed-production", "date"]
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
},
|
|
301
|
+
|
|
302
|
+
"PlanDef": {
|
|
303
|
+
"type": "object",
|
|
304
|
+
"required": ["id", "quotas", "features"],
|
|
305
|
+
"additionalProperties": false,
|
|
306
|
+
"properties": {
|
|
307
|
+
"id": { "type": "string", "pattern": "^[A-Z][A-Z0-9_]+$" },
|
|
308
|
+
"name": { "type": "string" },
|
|
309
|
+
"tagline": { "type": "string" },
|
|
310
|
+
"marketed": { "type": "boolean", "default": true },
|
|
311
|
+
"popular": { "type": "boolean", "default": false },
|
|
312
|
+
"monthlyNet": { "type": ["number", "null"], "minimum": 0 },
|
|
313
|
+
"yearlyNet": { "type": ["number", "null"], "minimum": 0 },
|
|
314
|
+
"quotas": {
|
|
315
|
+
"type": "object",
|
|
316
|
+
"patternProperties": {
|
|
317
|
+
"^[a-z][A-Za-z0-9]*$": { "type": "integer" }
|
|
318
|
+
},
|
|
319
|
+
"additionalProperties": false
|
|
320
|
+
},
|
|
321
|
+
"features": {
|
|
322
|
+
"type": "array",
|
|
323
|
+
"items": { "type": "string", "pattern": "^[A-Z][A-Z0-9_]+$" }
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|