@saasicat/spec 1.0.0-rc.2 → 1.0.0-rc.20

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.
Files changed (33) hide show
  1. package/README.md +30 -0
  2. package/acceptance/README.md +3 -3
  3. package/acceptance/mfa/totp-verify-good-and-bad-code.yaml +6 -6
  4. package/admin-api.openapi.yaml +328 -77
  5. package/cli-conventions.md +19 -19
  6. package/index.cjs +3 -0
  7. package/index.d.cts +3 -1
  8. package/index.d.ts +2 -0
  9. package/index.js +9 -1
  10. package/package.json +4 -2
  11. package/prisma-fragments/01-subscription.prisma +34 -47
  12. package/prisma-fragments/03-plan-versions.prisma +3 -4
  13. package/prisma-fragments/05-bundle.prisma +4 -5
  14. package/prisma-fragments/06-catalog-entries.prisma +24 -24
  15. package/prisma-fragments/07-promotion.prisma +2 -3
  16. package/prisma-fragments/08-subscription-contract.prisma +42 -5
  17. package/prisma-fragments/09-pending-registration.prisma +32 -27
  18. package/prisma-fragments/11-subscription-bundle.prisma +17 -0
  19. package/prisma-fragments/12-applied-settings.prisma +55 -0
  20. package/prisma-fragments/13-subscriber.prisma +94 -0
  21. package/prisma-fragments/14-payments.prisma +100 -0
  22. package/prisma-fragments/README.md +40 -22
  23. package/schemas/admin-manifest.schema.json +9 -2
  24. package/schemas/plan-catalog.schema.json +239 -18
  25. package/schemas/tenant-ledger.schema.json +236 -0
  26. package/sql/1.0-a-contract-names-its-subscriber.postgres.sql +383 -0
  27. package/sql/1.0-a-payment-method-is-a-gateway-reference.postgres.sql +177 -0
  28. package/sql/1.0-a-settings-change-carries-its-order.postgres.sql +73 -0
  29. package/sql/1.0-line-items-record-their-money.postgres.sql +134 -0
  30. package/sql/1.0-remove-project-key.postgres.sql +203 -0
  31. package/sql/1.0-the-applied-settings-are-recorded.postgres.sql +67 -0
  32. package/sql/constraints.postgres.sql +76 -0
  33. package/sql/reference-schema.postgres.sql +299 -77
@@ -17,10 +17,19 @@ openapi: 3.1.0
17
17
  # - Tenant end-customer API (self-service plan change) — separate file
18
18
  # - Onboarding API — separate file (uses PromoCode preview)
19
19
  # - X-Rechnung — its own package, its own OpenAPI file
20
+ #
21
+ # Who serves what: an operation marked `x-served-by: app` is part of this
22
+ # contract but NOT shipped by @saasicat/nest — the tenant model, platform user
23
+ # administration and the promo-code detail view reach into data the application
24
+ # owns, so the application serves those routes and the admin UI calls them.
25
+ # Everything else is served by the reference implementation.
26
+ # `tests/openapi-covers-the-implementation.test.js` holds both directions: no
27
+ # undocumented route, and no documented operation without an implementation or
28
+ # that marker.
20
29
 
21
30
  info:
22
31
  title: SaaS Platform SuperAdmin API
23
- version: 0.1.0-draft
32
+ version: 1.0.0-rc.20
24
33
  description: |
25
34
  Read and write operations for platform administration:
26
35
  tenants, users, subscriptions, promo codes, audit log,
@@ -48,6 +57,7 @@ tags:
48
57
  - name: mfa
49
58
  - name: manifest
50
59
  - name: catalog
60
+ - name: settings
51
61
 
52
62
  security:
53
63
  - bearerAuth: []
@@ -141,7 +151,7 @@ paths:
141
151
  # ────────────────────────────────────────────────────────
142
152
  # Dashboard
143
153
  # ────────────────────────────────────────────────────────
144
- /dashboard/stats:
154
+ /stats/dashboard:
145
155
  get:
146
156
  tags: [dashboard]
147
157
  summary: Global platform metrics
@@ -153,53 +163,6 @@ paths:
153
163
  schema:
154
164
  $ref: '#/components/schemas/DashboardStats'
155
165
 
156
- # ────────────────────────────────────────────────────────
157
- # Plan catalog (read-only — maintained via file + reload endpoint)
158
- # ────────────────────────────────────────────────────────
159
- /plans:
160
- get:
161
- tags: [plans]
162
- summary: Currently loaded plan catalog
163
- responses:
164
- '200':
165
- description: Catalog
166
- content:
167
- application/json:
168
- schema:
169
- $ref: '#/components/schemas/PlanCatalog'
170
-
171
- /plans/reload:
172
- post:
173
- tags: [plans]
174
- summary: Reload the plan catalog from file (for admin CLI / hot-reload dev)
175
- x-mfa-required: true
176
- responses:
177
- '200':
178
- description: Loaded
179
- content:
180
- application/json:
181
- schema:
182
- type: object
183
- properties:
184
- loadedAt: { type: string, format: date-time }
185
- planCount: { type: integer }
186
- source: { type: string, description: Path or URL }
187
-
188
- /plans/last-update:
189
- get:
190
- tags: [plans]
191
- summary: When the catalog was last loaded + hash
192
- responses:
193
- '200':
194
- description: Meta
195
- content:
196
- application/json:
197
- schema:
198
- type: object
199
- properties:
200
- loadedAt: { type: string, format: date-time }
201
- hash: { type: string, description: SHA-256 of the YAML content }
202
-
203
166
  # ────────────────────────────────────────────────────────
204
167
  # Tenants
205
168
  # ────────────────────────────────────────────────────────
@@ -234,6 +197,7 @@ paths:
234
197
 
235
198
  post:
236
199
  tags: [tenants]
200
+ x-served-by: app # the tenant model is the application's
237
201
  summary: Create a tenant manually (bypasses the onboarding flow)
238
202
  x-mfa-required: true
239
203
  requestBody:
@@ -262,6 +226,7 @@ paths:
262
226
  schema: { $ref: '#/components/schemas/AdminTenantDetail' }
263
227
  delete:
264
228
  tags: [tenants]
229
+ x-served-by: app # the tenant model is the application's
265
230
  summary: Soft-delete a tenant (set deletedAt)
266
231
  x-mfa-required: true
267
232
  requestBody:
@@ -315,6 +280,7 @@ paths:
315
280
  - $ref: '#/components/parameters/TenantSlug'
316
281
  post:
317
282
  tags: [tenants]
283
+ x-served-by: app # the tenant model is the application's
318
284
  summary: Short-lived JWT as TENANT_ADMIN for support insight
319
285
  x-mfa-required: true
320
286
  requestBody:
@@ -344,6 +310,7 @@ paths:
344
310
  - $ref: '#/components/parameters/TenantSlug'
345
311
  get:
346
312
  tags: [tenants]
313
+ x-served-by: app # the tenant model is the application's
347
314
  summary: GDPR / audit export of all tenant data as a ZIP
348
315
  x-mfa-required: true
349
316
  responses:
@@ -361,6 +328,7 @@ paths:
361
328
  - $ref: '#/components/parameters/TenantSlug'
362
329
  get:
363
330
  tags: [subscriptions]
331
+ x-served-by: app # no admin-side subscription route ships
364
332
  summary: Current subscription + add-ons + effective limits
365
333
  responses:
366
334
  '200':
@@ -371,6 +339,7 @@ paths:
371
339
 
372
340
  patch:
373
341
  tags: [subscriptions]
342
+ x-served-by: app # no admin-side subscription route ships
374
343
  summary: Change plan, custom limits, or pilot flag
375
344
  x-mfa-required: true
376
345
  requestBody:
@@ -435,6 +404,7 @@ paths:
435
404
  schema: { type: string }
436
405
  post:
437
406
  tags: [users]
407
+ x-served-by: app # platform users live in the application's identity store
438
408
  summary: Trigger a password-reset email (sends no plaintext passwords)
439
409
  x-mfa-required: true
440
410
  requestBody:
@@ -457,6 +427,7 @@ paths:
457
427
  schema: { type: string }
458
428
  post:
459
429
  tags: [users]
430
+ x-served-by: app # platform users live in the application's identity store
460
431
  summary: Deactivate a user (block login)
461
432
  x-mfa-required: true
462
433
  requestBody:
@@ -476,6 +447,7 @@ paths:
476
447
  - $ref: '#/components/parameters/TenantSlug'
477
448
  post:
478
449
  tags: [users]
450
+ x-served-by: app # the tenant model is the application's
479
451
  summary: Transfer the TENANT_ADMIN role to another user
480
452
  x-mfa-required: true
481
453
  requestBody:
@@ -546,6 +518,7 @@ paths:
546
518
  schema: { type: string, format: uuid }
547
519
  get:
548
520
  tags: [promo-codes]
521
+ x-served-by: app # the detail view joins application data
549
522
  responses:
550
523
  '200':
551
524
  description: Detail
@@ -622,41 +595,217 @@ paths:
622
595
  items: { $ref: '#/components/schemas/AuditEntryDto' }
623
596
 
624
597
  # ────────────────────────────────────────────────────────
625
- # MFA
598
+ # First-run setup (SetupModule, packages/nest/src/setup/*)
599
+ #
600
+ # Public by design: before the first SUPER_ADMIN there is no session to
601
+ # authenticate. The window is open only while zero SUPER_ADMIN exist and
602
+ # is additionally gated by the operator-set SETUP_TOKEN.
626
603
  # ────────────────────────────────────────────────────────
627
- /mfa/setup:
628
- post:
629
- tags: [mfa]
630
- summary: Start TOTP setup (returns QR-code URI + secret)
604
+ /setup/status:
605
+ get:
606
+ tags: [setup]
607
+ security: [] # PUBLIC — answers whether the window is still open
608
+ summary: Whether first-run setup is still open
631
609
  responses:
632
610
  '200':
633
- description: Setup data
611
+ description: Status
612
+ content:
613
+ application/json:
614
+ schema:
615
+ type: object
616
+ required: [needsSetup]
617
+ properties:
618
+ needsSetup:
619
+ type: boolean
620
+ description: true as long as no SUPER_ADMIN exists
621
+
622
+ /setup:
623
+ post:
624
+ tags: [setup]
625
+ security: [] # PUBLIC — see the section note
626
+ summary: Create the first SUPER_ADMIN and start its MFA enrolment
627
+ requestBody:
628
+ required: true
629
+ content:
630
+ application/json:
631
+ schema:
632
+ type: object
633
+ required: [token, email]
634
+ properties:
635
+ token: { type: string, description: Must match SETUP_TOKEN }
636
+ email: { type: string, format: email }
637
+ password:
638
+ type: string
639
+ minLength: 8
640
+ description: Generated by the server when omitted
641
+ responses:
642
+ '201':
643
+ description: Created — the response carries the enrolment secret once
634
644
  content:
635
645
  application/json:
636
646
  schema:
637
647
  type: object
648
+ required: [userId, email, otpauthUri, qrDataUrl, secret]
638
649
  properties:
650
+ userId: { type: string }
651
+ email: { type: string, format: email }
639
652
  otpauthUri: { type: string }
640
- secret: { type: string }
641
- recoveryCodes:
642
- type: array
643
- items: { type: string }
653
+ qrDataUrl: { type: string, description: PNG data URL }
654
+ secret: { type: string, description: Base32 TOTP secret }
655
+ generatedPassword:
656
+ type: string
657
+ description: Present only when the server generated it
658
+ '400':
659
+ description: The email is not an address the platform accepts
660
+ content:
661
+ application/json:
662
+ schema:
663
+ $ref: '#/components/schemas/ErrorResponse'
664
+ '401':
665
+ description: The token does not match SETUP_TOKEN
666
+ content:
667
+ application/json:
668
+ schema:
669
+ $ref: '#/components/schemas/ErrorResponse'
670
+ '403':
671
+ description: SETUP_TOKEN is not set — setup is disabled
672
+ content:
673
+ application/json:
674
+ schema:
675
+ $ref: '#/components/schemas/ErrorResponse'
676
+ '409':
677
+ description: >-
678
+ Either the window has closed itself because a SUPER_ADMIN already
679
+ exists (`SETUP_ALREADY_DONE`), or the address is taken by a platform
680
+ user (`EMAIL_EXISTS`). The `code` tells them apart.
681
+ content:
682
+ application/json:
683
+ schema:
684
+ $ref: '#/components/schemas/ErrorResponse'
644
685
 
645
- /mfa/confirm:
686
+ /setup/confirm-mfa:
646
687
  post:
647
- tags: [mfa]
648
- summary: Confirm setup with the first code
688
+ tags: [setup]
689
+ security: [] # PUBLIC — see the section note
690
+ summary: Confirm the enrolment with the first TOTP code, closing the window
649
691
  requestBody:
650
692
  required: true
651
693
  content:
652
694
  application/json:
653
695
  schema:
654
696
  type: object
655
- required: [code]
697
+ required: [token, userId, code]
656
698
  properties:
699
+ token: { type: string }
700
+ userId: { type: string }
657
701
  code: { type: string, pattern: '^[0-9]{6}$' }
658
702
  responses:
659
- '200': { description: Activated }
703
+ '200':
704
+ description: Activated
705
+ content:
706
+ application/json:
707
+ schema:
708
+ type: object
709
+ required: [ok]
710
+ properties:
711
+ ok: { type: boolean }
712
+ '401':
713
+ description: The token does not match SETUP_TOKEN
714
+ content:
715
+ application/json:
716
+ schema:
717
+ $ref: '#/components/schemas/ErrorResponse'
718
+ '403':
719
+ description: SETUP_TOKEN is not set — setup is disabled
720
+ content:
721
+ application/json:
722
+ schema:
723
+ $ref: '#/components/schemas/ErrorResponse'
724
+
725
+ # ────────────────────────────────────────────────────────
726
+ # Plan catalog import (PlanCatalogImporterModule)
727
+ # ────────────────────────────────────────────────────────
728
+ /billing/plan-catalog/import:
729
+ post:
730
+ tags: [plans]
731
+ summary: Import a plan catalog YAML into the catalog tables
732
+ requestBody:
733
+ required: true
734
+ content:
735
+ application/json:
736
+ schema:
737
+ type: object
738
+ required: [yamlContent]
739
+ properties:
740
+ yamlContent: { type: string, description: Contents of saas.yaml }
741
+ crossFieldChecks:
742
+ type: boolean
743
+ default: true
744
+ description: Validate references between plans, features and quotas
745
+ responses:
746
+ '201':
747
+ description: Import report — the counters say what was new and what already matched
748
+ content:
749
+ application/json:
750
+ schema:
751
+ type: object
752
+ required:
753
+ - plansCreated
754
+ - plansSkipped
755
+ - planVersionsCreated
756
+ - planVersionsSkipped
757
+ - featureEntriesCreated
758
+ - featureEntriesSkipped
759
+ - warnings
760
+ properties:
761
+ plansCreated: { type: integer }
762
+ plansSkipped: { type: integer }
763
+ planVersionsCreated: { type: integer }
764
+ planVersionsSkipped: { type: integer }
765
+ featureEntriesCreated: { type: integer }
766
+ featureEntriesSkipped: { type: integer }
767
+ warnings: { type: array, items: { type: string } }
768
+ '400':
769
+ description: The YAML failed schema validation
770
+ content:
771
+ application/json:
772
+ schema:
773
+ $ref: '#/components/schemas/ErrorResponse'
774
+
775
+ # ────────────────────────────────────────────────────────
776
+ # Subscriptions
777
+ # ────────────────────────────────────────────────────────
778
+ /subscriptions:
779
+ get:
780
+ tags: [subscriptions]
781
+ summary: Every tenant subscription, one row each
782
+ responses:
783
+ '200':
784
+ description: Rows
785
+ content:
786
+ application/json:
787
+ schema:
788
+ type: array
789
+ items:
790
+ type: object
791
+ required: [id, tenant, plan, status, billingCycle]
792
+ properties:
793
+ id: { type: string }
794
+ tenant:
795
+ type: object
796
+ required: [slug, name]
797
+ properties:
798
+ slug: { type: string }
799
+ name: { type: string }
800
+ plan: { type: string }
801
+ status: { type: string }
802
+ billingCycle: { type: string }
803
+ periodEndsAt:
804
+ type: string
805
+ format: date-time
806
+ nullable: true
807
+ monthlyNet: { type: string, nullable: true }
808
+ additionalProperties: true
660
809
 
661
810
  # ────────────────────────────────────────────────────────
662
811
  # Catalog V2 — Plan/Bundle/Marketing/Promotion + Discovery
@@ -1199,6 +1348,42 @@ paths:
1199
1348
  }
1200
1349
  responses: { '200': { description: Synchronized } }
1201
1350
 
1351
+ # ── The applied configuration (SettingsModule, packages/nest/src/settings/*) ──
1352
+ #
1353
+ # Read-only. What the installation is running on — read off the loaded
1354
+ # catalogue — and the record of it: since when, from where, and what changed
1355
+ # between two starts. The record is a mirror of config/saas.yaml, never a
1356
+ # source, so there is no write operation here.
1357
+
1358
+ /settings:
1359
+ get:
1360
+ tags: [settings]
1361
+ summary: The running configuration, since when it applies, and what changed at boot
1362
+ responses:
1363
+ '200':
1364
+ description: The applied settings and their record
1365
+ content:
1366
+ application/json:
1367
+ schema: { $ref: '#/components/schemas/AppliedSettingsView' }
1368
+
1369
+ /settings/changes/{id}/acknowledge:
1370
+ parameters: [{ name: id, in: path, required: true, schema: { type: string } }]
1371
+ post:
1372
+ tags: [settings]
1373
+ summary: Mark a recorded settings change as seen
1374
+ description: >-
1375
+ The record survives until this is called. The acknowledgement keeps its first
1376
+ author — a second call changes nothing and answers the record as it stands — and
1377
+ is written to the audit log as SETTINGS_CHANGE_ACKNOWLEDGE.
1378
+ responses:
1379
+ '200':
1380
+ description: The change, acknowledged
1381
+ content:
1382
+ application/json:
1383
+ schema: { $ref: '#/components/schemas/SettingsChangeView' }
1384
+ '404':
1385
+ description: No recorded change has this id (`SETTINGS_CHANGE_NOT_FOUND`)
1386
+
1202
1387
  # ── Discovery (scan/status, outside /catalog) ──
1203
1388
  /discovery:
1204
1389
  get:
@@ -1240,13 +1425,78 @@ components:
1240
1425
  schema: { type: string, pattern: '^[a-z0-9][a-z0-9-]{1,62}[a-z0-9]$' }
1241
1426
 
1242
1427
  schemas:
1428
+ # ────────── Applied settings (packages/nest/src/settings/settings.controller.ts) ──────────
1429
+ AppliedSettingsView:
1430
+ type: object
1431
+ required: [source, fingerprint, settings, recorded, appliedAt, changes]
1432
+ properties:
1433
+ source:
1434
+ {
1435
+ type: string,
1436
+ description: 'Where the running settings came from: the absolute path of the file, or a phrase saying they were handed in as code',
1437
+ }
1438
+ fingerprint:
1439
+ {
1440
+ type: string,
1441
+ description: 'sha256-<hex> over the canonical JSON of `settings`',
1442
+ }
1443
+ settings:
1444
+ type: object
1445
+ additionalProperties: true
1446
+ description: 'The settings subtree of the catalogue as it was resolved — everything but plans and features'
1447
+ recorded:
1448
+ {
1449
+ type: boolean,
1450
+ description: 'Whether this installation keeps a record at all (a persistence adapter provides the port)',
1451
+ }
1452
+ appliedAt:
1453
+ type: [string, 'null']
1454
+ format: date-time
1455
+ description: 'When these values became the running configuration. Null where nothing is recorded, or the record describes an earlier configuration.'
1456
+ changes:
1457
+ type: array
1458
+ description: 'Newest first; empty where nothing is recorded'
1459
+ items: { $ref: '#/components/schemas/SettingsChangeView' }
1460
+
1461
+ SettingsChangeView:
1462
+ type: object
1463
+ required: [id, noticedAt, source, differences, acknowledgedAt, acknowledgedBy]
1464
+ properties:
1465
+ id: { type: string }
1466
+ noticedAt:
1467
+ {
1468
+ type: string,
1469
+ format: date-time,
1470
+ description: 'The start that found the fingerprint moved',
1471
+ }
1472
+ source: { type: string }
1473
+ differences:
1474
+ type: array
1475
+ items: { $ref: '#/components/schemas/SettingsDifference' }
1476
+ acknowledgedAt: { type: [string, 'null'], format: date-time }
1477
+ acknowledgedBy:
1478
+ {
1479
+ type: [string, 'null'],
1480
+ description: 'An actor tag, as the audit log writes it',
1481
+ }
1482
+
1483
+ SettingsDifference:
1484
+ type: object
1485
+ required: [path]
1486
+ properties:
1487
+ path:
1488
+ {
1489
+ type: string,
1490
+ description: 'Dotted, as the loader names a field: tenantBilling.cancellationNoticeDays.monthly',
1491
+ }
1492
+ before: { description: 'Absent where the leaf did not exist before' }
1493
+ after: { description: 'Absent where the leaf no longer exists' }
1494
+
1243
1495
  # ────────── Catalog V2 (DTO = SSOT: packages/nest/src/catalog/dto/*) ──────────
1244
1496
  CatalogPlanCreate:
1245
1497
  type: object
1246
- required: [projectKey, planKey, label]
1498
+ required: [planKey, label]
1247
1499
  properties:
1248
- projectKey:
1249
- { type: string, pattern: '^[a-z][a-z0-9-]*$', description: 'kebab-case' }
1250
1500
  planKey:
1251
1501
  {
1252
1502
  type: string,
@@ -1287,9 +1537,8 @@ components:
1287
1537
 
1288
1538
  CatalogBundleCreate:
1289
1539
  type: object
1290
- required: [projectKey, bundleKey, label]
1540
+ required: [bundleKey, label]
1291
1541
  properties:
1292
- projectKey: { type: string, pattern: '^[a-z][a-z0-9-]*$' }
1293
1542
  bundleKey: { type: string, pattern: '^[A-Z][A-Z0-9_]*$', description: 'no hyphen' }
1294
1543
  label: { type: string, minLength: 1, maxLength: 120 }
1295
1544
  description: { type: string, maxLength: 2000 }
@@ -1347,9 +1596,8 @@ components:
1347
1596
 
1348
1597
  CatalogMarketingProjectionCreate:
1349
1598
  type: object
1350
- required: [projectKey, targetType, targetVersionId, displayLabel, description]
1599
+ required: [targetType, targetVersionId, displayLabel, description]
1351
1600
  properties:
1352
- projectKey: { type: string, pattern: '^[a-z][a-z0-9-]*$' }
1353
1601
  targetType: { type: string, enum: [PLAN, BUNDLE] }
1354
1602
  targetVersionId: { type: string }
1355
1603
  locale: { type: string, pattern: '^[a-z]{2}(-[A-Z]{2})?$', default: de }
@@ -1465,9 +1713,8 @@ components:
1465
1713
 
1466
1714
  CatalogMarketingSettingsUpdate:
1467
1715
  type: object
1468
- required: [projectKey, activeLocales]
1716
+ required: [activeLocales]
1469
1717
  properties:
1470
- projectKey: { type: string, pattern: '^[a-z][a-z0-9-]*$' }
1471
1718
  activeLocales:
1472
1719
  type: array
1473
1720
  uniqueItems: true
@@ -1475,9 +1722,8 @@ components:
1475
1722
 
1476
1723
  CatalogPromotionCreate:
1477
1724
  type: object
1478
- required: [projectKey, internalLabel, type, value, validFrom, validTo]
1725
+ required: [internalLabel, type, value, validFrom, validTo]
1479
1726
  properties:
1480
- projectKey: { type: string, pattern: '^[a-z][a-z0-9-]*$' }
1481
1727
  internalLabel: { type: string, maxLength: 120 }
1482
1728
  type: { type: string, enum: [percent, amount, intro, freeMonths] }
1483
1729
  value:
@@ -1550,10 +1796,15 @@ components:
1550
1796
  # ────────── Plan-Catalog (mirror of plans.yaml) ──────────
1551
1797
  PlanCatalog:
1552
1798
  type: object
1553
- required: [schemaVersion, projectKey, currency, vatRate, plans]
1799
+ required: [schemaVersion, app, currency, vatRate, plans]
1554
1800
  properties:
1555
1801
  schemaVersion: { type: integer, const: 1 }
1556
- projectKey: { type: string, description: 'Project key of the app (kebab-case)' }
1802
+ app:
1803
+ type: object
1804
+ required: [name]
1805
+ description: 'App identity — the only place the application is named'
1806
+ properties:
1807
+ name: { type: string, minLength: 1 }
1557
1808
  currency: { type: string, pattern: '^[A-Z]{3}$' }
1558
1809
  vatRate: { type: number, format: decimal, description: 'in percent' }
1559
1810
  features:
@@ -27,7 +27,7 @@ for the audit log:
27
27
  - Without an identity → writing commands are rejected with exit code `2`
28
28
  (see §6).
29
29
 
30
- Read commands (e.g. `… mandant list`) are allowed without an identity, but
30
+ Read commands (e.g. `… tenant list`) are allowed without an identity, but
31
31
  write `actor=anonymous` to the local log.
32
32
 
33
33
  ## 2. Mandatory MFA for Critical Operations
@@ -35,9 +35,9 @@ write `actor=anonymous` to the local log.
35
35
  The following operations **MUST** prompt for a TOTP code
36
36
  (Google Authenticator) before execution:
37
37
 
38
- - `… paket apply` (PlanCatalog mutation)
38
+ - `… plans apply` (PlanCatalog mutation)
39
39
  - `… pilot create|grant|revoke` (pilot override)
40
- - `… mandant suspend|impersonate` (tenant security operations)
40
+ - `… tenant suspend|impersonate` (tenant security operations)
41
41
  - `… plan-version publish` (PlanVersion publication)
42
42
  - `… user reassign-admin` (last-admin escalation)
43
43
  - `… admin mfa-reset` (MFA reset of another SUPER_ADMIN)
@@ -57,12 +57,12 @@ is not `NODE_ENV=development` and not a localhost DB), every
57
57
  writing command must be confirmed interactively:
58
58
 
59
59
  ```text
60
- ? Tippe production zur Bestätigung: production
60
+ ? Type production to confirm: production
61
61
  ```
62
62
 
63
63
  - Alternative: `--yes` / `-y` skips the confirmation (for CI/CD).
64
64
  - Plus `--dry-run` is the default for destructive commands like
65
- `… paket apply` and `… rabatt delete`. Only `--apply` or
65
+ `… plans apply` and `… discounts delete`. Only `--apply` or
66
66
  `--yes` applies.
67
67
 
68
68
  `production` detection should run through the consumer's implementation of the
@@ -110,11 +110,11 @@ redefine them, because cron/CI scripts pattern-match on them:
110
110
  | 4 | Connectivity error (DB unreachable, sidecar service down) |
111
111
  | 5 | Permission error (user is not allowed to perform this operation — e.g. a SUPER_ADMIN operation, but the user is only a TENANT_ADMIN) |
112
112
  | 6 | Conflict (optimistic-lock mismatch, idempotency violation) |
113
- | 7 | Drift detected (e.g. `paket diff` finds differences; `manifest check` finds inconsistencies) |
113
+ | 7 | Drift detected (e.g. `plans diff` finds differences; `manifest check` finds inconsistencies) |
114
114
  | 99 | Internal error (uncaught exception, bug reports welcome) |
115
115
 
116
116
  Read commands return `0` even for an empty result set (no drift =
117
- no error). Drift-detection commands (`paket diff`, `manifest check`)
117
+ no error). Drift-detection commands (`plans diff`, `manifest check`)
118
118
  return `7` when drift is found — CI gates can react to that.
119
119
 
120
120
  ## 7. Consumer Plugin API
@@ -136,23 +136,23 @@ Plugin commands automatically inherit:
136
136
 
137
137
  ```bash
138
138
  # Read operation, no identity required
139
- $ myapp mandant list --output=json | jq '.[] | select(.status=="ACTIVE")'
139
+ $ myapp tenant list --output=json | jq '.[] | select(.status=="ACTIVE")'
140
140
 
141
141
  # Writing with identity, dry-run as default
142
- $ myapp paket apply config/plans.yaml
143
- ℹ Diff: 2 Pläne aktualisiert, 1 Bundle neu.
144
- ℹ Dry-run — nutze --apply zum Schreiben.
142
+ $ myapp plans apply config/plans.yaml
143
+ ℹ Diff: 2 plans updated, 1 bundle added.
144
+ ℹ Dry run — pass --apply to write.
145
145
 
146
146
  # Writing with mandatory MFA
147
- $ myapp paket apply config/plans.yaml --apply
148
- ℹ Erfordert MFA-Bestätigung.
149
- ? TOTP-Code: 482 159
150
- ✓ PlanCatalog aktualisiert. AuditLog: PLAN_CATALOG_UPDATE.
147
+ $ myapp plans apply config/plans.yaml --apply
148
+ ℹ MFA confirmation required.
149
+ ? TOTP code: 482 159
150
+ ✓ PlanCatalog updated. AuditLog: PLAN_CATALOG_UPDATE.
151
151
 
152
152
  # Production confirm
153
153
  $ NODE_ENV=production myapp pilot grant pilot-schmidt --as=admin@example.com
154
- ? Tippe production zur Bestätigung: production
155
- ℹ Erfordert MFA-Bestätigung.
156
- ? TOTP-Code: 217 998
157
- ✓ Pilot-Grant gespeichert.
154
+ ? Type production to confirm: production
155
+ ℹ MFA confirmation required.
156
+ ? TOTP code: 217 998
157
+ ✓ Pilot grant stored.
158
158
  ```