@saasicat/spec 1.0.0-rc.24 → 1.0.0-rc.26

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 (27) hide show
  1. package/admin-api.openapi.yaml +888 -2
  2. package/package.json +1 -1
  3. package/prisma-fragments/05-bundle.prisma +4 -3
  4. package/prisma-fragments/08-subscription-contract.prisma +9 -0
  5. package/prisma-fragments/09-pending-registration.prisma +6 -0
  6. package/prisma-fragments/11-subscription-bundle.prisma +12 -2
  7. package/prisma-fragments/13-subscriber.prisma +82 -8
  8. package/prisma-fragments/15-subscriber-ledger.prisma +2 -1
  9. package/prisma-fragments/17-subscription-notice.prisma +5 -2
  10. package/prisma-fragments/19-bundle-version-retirement.prisma +44 -0
  11. package/prisma-fragments/20-subscription-invoice.prisma +145 -0
  12. package/prisma-fragments/21-feature-withdrawal.prisma +56 -0
  13. package/prisma-fragments/22-subscription-invoice-archive.prisma +34 -0
  14. package/prisma-fragments/README.md +26 -20
  15. package/schemas/admin-manifest.schema.json +2 -1
  16. package/schemas/plan-catalog.schema.json +57 -2
  17. package/schemas/subscriber-ledger.schema.json +4 -2
  18. package/sql/1.0-a-correction-carries-its-order.postgres.sql +101 -0
  19. package/sql/1.0-a-feature-is-withdrawn.postgres.sql +49 -0
  20. package/sql/1.0-a-sign-up-keeps-its-vat-id-check.postgres.sql +38 -0
  21. package/sql/1.0-a-subscriber-has-a-tax-origin.postgres.sql +113 -0
  22. package/sql/1.0-an-add-on-retirement-is-announced.postgres.sql +44 -0
  23. package/sql/1.0-an-add-on-switch-waits-for-its-term.postgres.sql +52 -0
  24. package/sql/1.0-an-invoice-document-is-archived.postgres.sql +123 -0
  25. package/sql/1.0-an-invoice-is-issued.postgres.sql +171 -0
  26. package/sql/constraints.postgres.sql +17 -0
  27. package/sql/reference-schema.postgres.sql +250 -1
@@ -29,7 +29,7 @@ openapi: 3.1.0
29
29
 
30
30
  info:
31
31
  title: SaaS Platform SuperAdmin API
32
- version: 1.0.0-rc.24
32
+ version: 1.0.0-rc.26
33
33
  description: |
34
34
  Read and write operations for platform administration:
35
35
  tenants, users, subscriptions, promo codes, audit log,
@@ -387,6 +387,240 @@ paths:
387
387
  '404':
388
388
  description: No tenant with this slug (`TENANT_NOT_FOUND`)
389
389
 
390
+ /invoices/{id}/document:
391
+ parameters:
392
+ - name: id
393
+ in: path
394
+ required: true
395
+ description: The invoice's id
396
+ schema: { type: string }
397
+ get:
398
+ tags: [subscriptions]
399
+ summary: The document an invoice was issued as
400
+ description: >-
401
+ Served where the platform keeps invoices
402
+ (`tenantBilling.chargeJournal.invoices`) and shows tenants
403
+ (`adminResources`); the manifest announces it as
404
+ `invoices.read`. Answers the file archived for the invoice,
405
+ checked against the checksum recorded when it was rendered —
406
+ never a new rendering. Its media type is the one the renderer
407
+ gave, a PDF for a ZUGFeRD invoice.
408
+ responses:
409
+ '200':
410
+ description: The archived file, as an attachment
411
+ headers:
412
+ Content-Disposition:
413
+ description: >-
414
+ `attachment; filename="…"`, the renderer's file
415
+ name kept to letters, digits, `.`, `_` and `-`
416
+ schema: { type: string }
417
+ content:
418
+ application/pdf:
419
+ schema: { type: string, format: binary }
420
+ '404':
421
+ description: No invoice with this id (`SUBSCRIPTION_INVOICE_NOT_FOUND`)
422
+ '409':
423
+ description: >-
424
+ The invoice still waits for its document
425
+ (`SUBSCRIPTION_INVOICE_DOCUMENT_NOT_ARCHIVED`), or its
426
+ archived file does not match the recorded checksum and
427
+ is not handed out (`SUBSCRIPTION_INVOICE_DOCUMENT_MISMATCH`)
428
+
429
+ /tenants/{slug}/subscriber:
430
+ parameters:
431
+ - $ref: '#/components/parameters/TenantSlug'
432
+ get:
433
+ tags: [subscriptions]
434
+ summary: The tenant's subscriber, its tax details and what holds its next contract back
435
+ description: >-
436
+ Served where the platform keeps subscribers and shows tenants
437
+ (`adminResources`); the manifest announces it as
438
+ `subscribers.read`. The standing is computed when it is read,
439
+ and only where a tax adapter decides; elsewhere `readiness` is
440
+ null.
441
+ responses:
442
+ '200':
443
+ description: The subscriber, or none
444
+ content:
445
+ application/json:
446
+ schema: { $ref: '#/components/schemas/AdminTenantSubscriber' }
447
+ '404':
448
+ description: No tenant with this slug (`TENANT_NOT_FOUND`)
449
+
450
+ /tenants/{slug}/subscriber/history:
451
+ parameters:
452
+ - $ref: '#/components/parameters/TenantSlug'
453
+ get:
454
+ tags: [subscriptions]
455
+ summary: Every correction, change of country or business status, and check of the subscriber, the latest first
456
+ description: >-
457
+ Served beside `/tenants/{slug}/subscriber`; the manifest
458
+ announces it as `subscribers.correct`. A correction that gives
459
+ another VAT number is named once, as the correction it was. Empty
460
+ for a tenant without a subscriber.
461
+ responses:
462
+ '200':
463
+ description: The history
464
+ content:
465
+ application/json:
466
+ schema:
467
+ type: object
468
+ required: [entries]
469
+ properties:
470
+ entries:
471
+ type: array
472
+ items:
473
+ $ref: '#/components/schemas/AdminSubscriberHistoryEntry'
474
+ '404':
475
+ description: No tenant with this slug (`TENANT_NOT_FOUND`)
476
+
477
+ /tenants/{slug}/subscriber/identity:
478
+ parameters:
479
+ - $ref: '#/components/parameters/TenantSlug'
480
+ post:
481
+ tags: [subscriptions]
482
+ summary: Correct the legal identity of the tenant's subscriber
483
+ description: >-
484
+ A correction of the same legal entity — a field left out keeps
485
+ its value, `null` clears a tax identifier — recorded with the
486
+ values it replaced, the reason and who made it. `kind` is the
487
+ operator's declaration: a takeover by another entity is refused
488
+ (`SUBSCRIBER_TAKEOVER_IS_A_TRANSFER`). Where a tax adapter
489
+ decides and the correction gives another VAT number, that
490
+ number is checked right after and the check recorded, whatever
491
+ it found; the answer says how it went.
492
+ x-mfa-required: true
493
+ requestBody:
494
+ required: true
495
+ content:
496
+ application/json:
497
+ schema:
498
+ type: object
499
+ required: [kind, reason]
500
+ properties:
501
+ kind: { type: string, enum: [correction, takeover] }
502
+ legalName: { type: string }
503
+ vatId: { type: string, nullable: true }
504
+ taxNumber: { type: string, nullable: true }
505
+ reason: { type: string, maxLength: 500 }
506
+ responses:
507
+ '200':
508
+ description: The subscriber as it now stands, and the check its new number got
509
+ content:
510
+ application/json:
511
+ schema: { $ref: '#/components/schemas/AdminSubscriberCorrected' }
512
+ '404':
513
+ description: No tenant with this slug (`TENANT_NOT_FOUND`)
514
+ '409':
515
+ description: The tenant has no subscriber (`SUBSCRIBER_REQUIRED`)
516
+ '422':
517
+ description: >-
518
+ A takeover, no reason, or a correction that changes
519
+ nothing (`SUBSCRIBER_TAKEOVER_IS_A_TRANSFER`,
520
+ `SUBSCRIBER_CORRECTION_REASON_REQUIRED`,
521
+ `SUBSCRIBER_CORRECTION_CHANGES_NOTHING`)
522
+
523
+ /tenants/{slug}/subscriber/business-status:
524
+ parameters:
525
+ - $ref: '#/components/parameters/TenantSlug'
526
+ post:
527
+ tags: [subscriptions]
528
+ summary: Record whether the tenant's subscriber acts as a business, and why
529
+ description: >-
530
+ Recorded as a change of the tax origin with its reason and who
531
+ made it; it applies from the next contract. Setting the status it
532
+ has records nothing.
533
+ x-mfa-required: true
534
+ requestBody:
535
+ required: true
536
+ content:
537
+ application/json:
538
+ schema:
539
+ type: object
540
+ required: [business, reason]
541
+ properties:
542
+ business: { type: boolean, nullable: true }
543
+ reason: { type: string, maxLength: 500 }
544
+ responses:
545
+ '200':
546
+ description: The subscriber as it now stands
547
+ content:
548
+ application/json:
549
+ schema: { $ref: '#/components/schemas/AdminSubscriberCorrected' }
550
+ '404':
551
+ description: No tenant with this slug (`TENANT_NOT_FOUND`)
552
+ '409':
553
+ description: The tenant has no subscriber (`SUBSCRIBER_REQUIRED`)
554
+ '422':
555
+ description: No reason (`SUBSCRIBER_BUSINESS_STATUS_REASON_REQUIRED`)
556
+
557
+ /tenants/{slug}/subscriber/vat-id-check:
558
+ parameters:
559
+ - $ref: '#/components/parameters/TenantSlug'
560
+ post:
561
+ tags: [subscriptions]
562
+ summary: Check the VAT number the tenant's subscriber holds, again
563
+ description: >-
564
+ Asks the service the tax adapter names and records a completed
565
+ check, valid or not; a check that does not complete records
566
+ nothing and says why. No second factor: nothing is entered, and
567
+ the answer is the service's.
568
+ responses:
569
+ '200':
570
+ description: The subscriber as it now stands, and what the check found
571
+ content:
572
+ application/json:
573
+ schema: { $ref: '#/components/schemas/AdminSubscriberCorrected' }
574
+ '404':
575
+ description: No tenant with this slug (`TENANT_NOT_FOUND`)
576
+ '409':
577
+ description: >-
578
+ The tenant has no subscriber, or no tax adapter names a
579
+ service (`SUBSCRIBER_REQUIRED`, `TAX_VAT_ID_CHECK_NOT_AVAILABLE`)
580
+ '422':
581
+ description: The subscriber holds no VAT number (`SUBSCRIBER_VAT_ID_MISSING`)
582
+
583
+ /subscribers/attention:
584
+ get:
585
+ tags: [subscriptions]
586
+ summary: Which of these tenants hold their subscriber back from its next contract
587
+ description: >-
588
+ Served beside `/tenants/{slug}/subscriber`; the manifest
589
+ announces `subscribers.attention` only where a tax adapter
590
+ decides, and without one the answer is always empty. A tenant
591
+ whose subscriber is ready, and one without a subscriber, are
592
+ left out.
593
+ parameters:
594
+ - name: tenantId
595
+ in: query
596
+ description: The tenants of the page shown; repeat the parameter for each.
597
+ schema:
598
+ type: array
599
+ maxItems: 200
600
+ items: { type: string, minLength: 1 }
601
+ style: form
602
+ explode: true
603
+ responses:
604
+ '200':
605
+ description: The tenants held back, each with why
606
+ content:
607
+ application/json:
608
+ schema:
609
+ type: object
610
+ required: [attention]
611
+ properties:
612
+ attention:
613
+ type: array
614
+ items:
615
+ type: object
616
+ required: [tenantId, readiness]
617
+ properties:
618
+ tenantId: { type: string }
619
+ readiness:
620
+ $ref: '#/components/schemas/SubscriberReadiness'
621
+ '400':
622
+ description: More than 200 tenants, or an empty tenant id
623
+
390
624
  # ────────────────────────────────────────────────────────
391
625
  # Users
392
626
  # ────────────────────────────────────────────────────────
@@ -812,8 +1046,9 @@ paths:
812
1046
  id: { type: string }
813
1047
  tenant:
814
1048
  type: object
815
- required: [slug, name]
1049
+ required: [id, slug, name]
816
1050
  properties:
1051
+ id: { type: string }
817
1052
  slug: { type: string }
818
1053
  name: { type: string }
819
1054
  plan: { type: string }
@@ -1084,6 +1319,203 @@ paths:
1084
1319
  type: array
1085
1320
  items: { $ref: '#/components/schemas/VersionRetirement' }
1086
1321
 
1322
+ # Served where the manifest carries the capability `bundleVersions.retire`. The bookings
1323
+ # on an add-on version no longer on sale continue on a version of the same add-on on
1324
+ # sale, each at the first end of its own period at least three calendar months after
1325
+ # its notice reached the subscriber, and may be cancelled without their minimum term
1326
+ # until then. Where the operator's terms are not confirmed to allow it, both routes
1327
+ # refuse with `RETIREMENT_TERMS_NOT_CONFIRMED` before reading anything.
1328
+
1329
+ /catalog/bundle-versions/{id}/retirement:
1330
+ parameters: [{ name: id, in: path, required: true, schema: { type: string } }]
1331
+ get:
1332
+ tags: [catalog]
1333
+ summary: What retiring the add-on version for its running bookings would do now
1334
+ parameters:
1335
+ - name: replacement
1336
+ in: query
1337
+ required: true
1338
+ description: The version of the same add-on the bookings continue on
1339
+ schema: { type: string, format: uuid }
1340
+ responses:
1341
+ '200':
1342
+ description: >-
1343
+ Which bookings it reaches and when, which it does not and why, and every
1344
+ blocker the announcement would refuse with
1345
+ content:
1346
+ application/json:
1347
+ schema: { $ref: '#/components/schemas/BundleRetirementPreview' }
1348
+ '404':
1349
+ description: Either version does not exist (`BUNDLE_VERSION_NOT_FOUND`)
1350
+ '422':
1351
+ description: >-
1352
+ The terms are not confirmed (`RETIREMENT_TERMS_NOT_CONFIRMED`), or the
1353
+ replacement is the version itself (`RETIREMENT_REPLACEMENT_IS_RETIRED`)
1354
+ post:
1355
+ tags: [catalog]
1356
+ summary: Announce the add-on retirement to every booking it reaches
1357
+ description: >-
1358
+ The announcement and one notice per booking are recorded in one transaction,
1359
+ then handed to the application's `SubscriptionNoticePort` as
1360
+ `bundle-version-retired`; a notice that cannot be sent now is sent by the next
1361
+ run. Audited as BUNDLE_VERSION_RETIRE.
1362
+ x-mfa-required: true
1363
+ requestBody:
1364
+ required: true
1365
+ content:
1366
+ application/json:
1367
+ schema: { $ref: '#/components/schemas/BundleRetirementAnnouncement' }
1368
+ responses:
1369
+ '201':
1370
+ description: Announced
1371
+ content:
1372
+ application/json:
1373
+ schema: { $ref: '#/components/schemas/BundleRetirementAnnounced' }
1374
+ '409':
1375
+ description: >-
1376
+ The bookings it reaches are no longer the ones named
1377
+ (`RETIREMENT_PREVIEW_CHANGED`); the body carries the preview as it stands
1378
+ '422':
1379
+ description: >-
1380
+ The first blocker the preview reports (`BUNDLE_RETIREMENT_*`,
1381
+ `RETIREMENT_WITHIN_TWELVE_MONTHS`), with every blocker beside it
1382
+
1383
+ /catalog/bundle-version-retirements:
1384
+ get:
1385
+ tags: [catalog]
1386
+ summary: Every add-on retirement announced, the most recent first
1387
+ responses:
1388
+ '200':
1389
+ description: List, each with how far it has come over the bookings it reached
1390
+ content:
1391
+ application/json:
1392
+ schema:
1393
+ type: array
1394
+ items: { $ref: '#/components/schemas/BundleVersionRetirement' }
1395
+
1396
+ # ── Feature withdrawals ──
1397
+ #
1398
+ # Served where the manifest carries the capability `features.withdraw`: where the
1399
+ # installation can list everybody a withdrawal reaches. A feature withdrawn for a reason
1400
+ # outside the platform — an external service stopped, a law changed — is granted to
1401
+ # nobody from the date named until the withdrawal is lifted. The subscriptions holding
1402
+ # it when it is announced are told at once; each line is reduced by the amount named
1403
+ # for its plan or add-on and rhythm, and each may end at once while it is withdrawn.
1404
+
1405
+ /feature-withdrawals:
1406
+ get:
1407
+ tags: [catalog]
1408
+ summary: Every withdrawal, the most recently announced first
1409
+ responses:
1410
+ '200':
1411
+ description: List, each with how many it reached, told and ended at once
1412
+ content:
1413
+ application/json:
1414
+ schema:
1415
+ type: array
1416
+ items: { $ref: '#/components/schemas/FeatureWithdrawal' }
1417
+ post:
1418
+ tags: [catalog]
1419
+ summary: Withdraw a feature from every subscription that holds it
1420
+ description: >-
1421
+ The withdrawal and one notice per subscription are recorded in one transaction,
1422
+ then handed to the application's `SubscriptionNoticePort` as `feature-withdrawn`;
1423
+ a notice that cannot be sent now is sent by the next run. The reductions are
1424
+ written into each subscription's contract. Audited as FEATURE_WITHDRAW.
1425
+ x-mfa-required: true
1426
+ requestBody:
1427
+ required: true
1428
+ content:
1429
+ application/json:
1430
+ schema: { $ref: '#/components/schemas/FeatureWithdrawalAnnouncement' }
1431
+ responses:
1432
+ '201':
1433
+ description: Announced
1434
+ content:
1435
+ application/json:
1436
+ schema: { $ref: '#/components/schemas/FeatureWithdrawalAnnounced' }
1437
+ '404':
1438
+ description: >-
1439
+ Not offered here: the stores cannot list everybody a withdrawal reaches
1440
+ (`FEATURE_WITHDRAWAL_UNAVAILABLE`)
1441
+ '409':
1442
+ description: >-
1443
+ The subscriptions it reaches are no longer the ones named
1444
+ (`FEATURE_WITHDRAWAL_PREVIEW_CHANGED`, with the preview as it stands), or
1445
+ another withdrawal of the feature landed first (`FEATURE_WITHDRAWAL_OPEN`)
1446
+ '422':
1447
+ description: >-
1448
+ An unknown feature (`FEATURE_WITHDRAWAL_FEATURE_UNKNOWN`), a date already
1449
+ past (`FEATURE_WITHDRAWAL_DATE_IN_PAST`), a reduction that names nothing it
1450
+ reaches, is named twice or exceeds the lowest price it reduces
1451
+ (`FEATURE_WITHDRAWAL_REDUCTION_*`), or the first blocker the preview reports
1452
+
1453
+ /feature-withdrawals/preview:
1454
+ get:
1455
+ tags: [catalog]
1456
+ summary: What withdrawing the feature from the date would do now
1457
+ parameters:
1458
+ - name: featureKey
1459
+ in: query
1460
+ required: true
1461
+ schema: { type: string }
1462
+ - name: effectiveFrom
1463
+ in: query
1464
+ required: false
1465
+ description: ISO-8601; now where left out
1466
+ schema: { type: string, format: date-time }
1467
+ responses:
1468
+ '200':
1469
+ description: >-
1470
+ Whom it reaches with which lines, whom it does not and why, the plans and
1471
+ add-ons a reduction may be named for, and every blocker
1472
+ content:
1473
+ application/json:
1474
+ schema: { $ref: '#/components/schemas/FeatureWithdrawalPreview' }
1475
+ '404':
1476
+ description: Not offered here (`FEATURE_WITHDRAWAL_UNAVAILABLE`)
1477
+ '422':
1478
+ description: >-
1479
+ An unknown feature (`FEATURE_WITHDRAWAL_FEATURE_UNKNOWN`), or a date already
1480
+ past (`FEATURE_WITHDRAWAL_DATE_IN_PAST`)
1481
+
1482
+ /feature-withdrawals/{id}/lift:
1483
+ parameters: [{ name: id, in: path, required: true, schema: { type: string } }]
1484
+ post:
1485
+ tags: [catalog]
1486
+ summary: Lift a withdrawal from a date
1487
+ description: >-
1488
+ From the date the feature is granted again, and the reduction and the right to end
1489
+ at once end; every subscription it reached that still runs is told as
1490
+ `feature-withdrawal-lifted`. Audited as FEATURE_WITHDRAWAL_LIFT.
1491
+ x-mfa-required: true
1492
+ requestBody:
1493
+ required: true
1494
+ content:
1495
+ application/json:
1496
+ schema:
1497
+ type: object
1498
+ properties:
1499
+ liftedFrom:
1500
+ type: string
1501
+ format: date-time
1502
+ description: ISO-8601; now where left out
1503
+ responses:
1504
+ '201':
1505
+ description: Lifted
1506
+ content:
1507
+ application/json:
1508
+ schema: { $ref: '#/components/schemas/FeatureWithdrawalLifted' }
1509
+ '404':
1510
+ description: No withdrawal has that id (`FEATURE_WITHDRAWAL_NOT_FOUND`)
1511
+ '409':
1512
+ description: >-
1513
+ Lifted already (`FEATURE_WITHDRAWAL_ALREADY_LIFTED`, with its date), or nobody
1514
+ it reaches told of it yet, telling them having failed just now
1515
+ (`FEATURE_WITHDRAWAL_NOT_TOLD_YET`)
1516
+ '422':
1517
+ description: A date already past (`FEATURE_WITHDRAWAL_LIFT_IN_PAST`)
1518
+
1087
1519
  # ── Bundles ──
1088
1520
  /catalog/bundles:
1089
1521
  get:
@@ -2114,6 +2546,327 @@ components:
2114
2546
  told: { type: integer, description: 'Notices sent at once' }
2115
2547
  failed: { type: integer, description: 'Notices the next run sends' }
2116
2548
 
2549
+ FeatureWithdrawalReduction:
2550
+ type: object
2551
+ required: [kind, key, billingCycle, amountNet]
2552
+ properties:
2553
+ kind: { type: string, enum: [plan, bundle] }
2554
+ key: { type: string, description: 'The plan key, or the add-on key' }
2555
+ billingCycle: { type: string, enum: [MONTHLY, YEARLY] }
2556
+ amountNet:
2557
+ type: number
2558
+ description: 'Net, per whole period of the rhythm, to the cent'
2559
+
2560
+ FeatureWithdrawalAnnouncement:
2561
+ type: object
2562
+ required: [featureKey, reason, reductions, subscriptionIds]
2563
+ properties:
2564
+ featureKey: { type: string }
2565
+ reason: { type: string, description: 'Shown to the subscribers as written' }
2566
+ effectiveFrom:
2567
+ { type: string, format: date-time, description: 'Now where left out' }
2568
+ reductions:
2569
+ type: array
2570
+ items: { $ref: '#/components/schemas/FeatureWithdrawalReduction' }
2571
+ subscriptionIds:
2572
+ type: array
2573
+ description: 'The subscriptions the operator was shown in the preview'
2574
+ items: { type: string }
2575
+
2576
+ FeatureWithdrawalRow:
2577
+ type: object
2578
+ required:
2579
+ [
2580
+ id,
2581
+ featureKey,
2582
+ reason,
2583
+ effectiveFrom,
2584
+ liftedFrom,
2585
+ reductions,
2586
+ announcedAt,
2587
+ announcedBy,
2588
+ liftedAt,
2589
+ liftedBy,
2590
+ toldAt,
2591
+ ]
2592
+ properties:
2593
+ id: { type: string }
2594
+ featureKey: { type: string }
2595
+ reason: { type: string }
2596
+ effectiveFrom: { type: string, format: date-time }
2597
+ liftedFrom: { type: [string, 'null'], format: date-time }
2598
+ reductions:
2599
+ type: array
2600
+ items: { $ref: '#/components/schemas/FeatureWithdrawalReduction' }
2601
+ announcedAt: { type: string, format: date-time }
2602
+ announcedBy:
2603
+ { type: string, description: 'An actor tag, as the audit log writes it' }
2604
+ liftedAt: { type: [string, 'null'], format: date-time }
2605
+ liftedBy: { type: [string, 'null'] }
2606
+ toldAt:
2607
+ type: [string, 'null']
2608
+ format: date-time
2609
+ description: >-
2610
+ When the subscriptions it reaches were told; null until then — the
2611
+ quarter-hourly run tells a withdrawal still null.
2612
+
2613
+ FeatureWithdrawal:
2614
+ allOf:
2615
+ - $ref: '#/components/schemas/FeatureWithdrawalRow'
2616
+ - type: object
2617
+ required: [featureLabel, progress]
2618
+ properties:
2619
+ featureLabel: { type: string }
2620
+ progress:
2621
+ type: object
2622
+ required: [reached, told, endedAtOnce]
2623
+ properties:
2624
+ reached: { type: integer, description: 'Subscriptions it reached' }
2625
+ told:
2626
+ type: integer
2627
+ description: 'Of those, the ones whose notice reached somebody'
2628
+ endedAtOnce:
2629
+ type: integer
2630
+ description: 'Subscriptions and bookings ended at once under it'
2631
+
2632
+ FeatureWithdrawalAnnounced:
2633
+ type: object
2634
+ required: [withdrawal, told, failed]
2635
+ properties:
2636
+ withdrawal: { $ref: '#/components/schemas/FeatureWithdrawalRow' }
2637
+ told: { type: integer, description: 'Notices sent at once' }
2638
+ failed: { type: integer, description: 'Notices the next run sends' }
2639
+
2640
+ FeatureWithdrawalLifted:
2641
+ type: object
2642
+ required: [withdrawal, told, failed]
2643
+ properties:
2644
+ withdrawal: { $ref: '#/components/schemas/FeatureWithdrawalRow' }
2645
+ told: { type: integer, description: 'Notices sent at once' }
2646
+ failed: { type: integer, description: 'Notices the next run sends' }
2647
+
2648
+ FeatureWithdrawalPreview:
2649
+ type: object
2650
+ required: [feature, effectiveFrom, asOf, reached, skipped, targets, blockers]
2651
+ properties:
2652
+ feature:
2653
+ type: object
2654
+ required: [key, label]
2655
+ properties:
2656
+ key: { type: string }
2657
+ label: { type: string }
2658
+ effectiveFrom: { type: string, description: 'ISO-8601' }
2659
+ asOf: { type: string, description: 'ISO-8601: the moment it is computed for' }
2660
+ reached:
2661
+ type: array
2662
+ items:
2663
+ type: object
2664
+ required: [tenantId, subscriptionId, status, lines, specialTerms]
2665
+ properties:
2666
+ tenantId: { type: string }
2667
+ subscriptionId: { type: string }
2668
+ status: { type: string }
2669
+ lines:
2670
+ type: array
2671
+ items:
2672
+ type: object
2673
+ required:
2674
+ [
2675
+ line,
2676
+ key,
2677
+ label,
2678
+ subscriptionBundleId,
2679
+ billingCycle,
2680
+ priceNet,
2681
+ ]
2682
+ properties:
2683
+ line: { type: string, enum: [plan, bundle] }
2684
+ key: { type: string }
2685
+ label: { type: string }
2686
+ subscriptionBundleId: { type: [string, 'null'] }
2687
+ billingCycle: { type: string, enum: [MONTHLY, YEARLY] }
2688
+ priceNet: { type: [number, 'null'] }
2689
+ specialTerms:
2690
+ type: boolean
2691
+ description: 'Holds the feature through special terms, and no line'
2692
+ skipped:
2693
+ type: array
2694
+ items:
2695
+ type: object
2696
+ required: [tenantId, subscriptionId, reason]
2697
+ properties:
2698
+ tenantId: { type: string }
2699
+ subscriptionId: { type: string }
2700
+ reason: { type: string, enum: [ends-before] }
2701
+ targets:
2702
+ type: array
2703
+ description: 'Each plan and add-on in a rhythm a reduction may be named for'
2704
+ items:
2705
+ type: object
2706
+ required: [kind, key, label, billingCycle, lines, lowestPriceNet]
2707
+ properties:
2708
+ kind: { type: string, enum: [plan, bundle] }
2709
+ key: { type: string }
2710
+ label: { type: string }
2711
+ billingCycle: { type: string, enum: [MONTHLY, YEARLY] }
2712
+ lines: { type: integer }
2713
+ lowestPriceNet: { type: [number, 'null'] }
2714
+ blockers:
2715
+ type: array
2716
+ description: 'Empty where it may be announced as it stands'
2717
+ items:
2718
+ type: object
2719
+ required: [code, message, params]
2720
+ properties:
2721
+ code: { type: string, description: 'FEATURE_WITHDRAWAL_*' }
2722
+ message: { type: string }
2723
+ params: { type: object }
2724
+
2725
+ RetiredBundleVersionRef:
2726
+ type: object
2727
+ required: [bundleVersionId, bundleKey, version]
2728
+ properties:
2729
+ bundleVersionId: { type: string }
2730
+ bundleKey: { type: string }
2731
+ version: { type: integer }
2732
+
2733
+ BundleVersionSide:
2734
+ description: >-
2735
+ An add-on version as the subscribers compare it, at its own prices; each booking
2736
+ is told the prices beside its plan. A price of null is a rhythm it is not sold in
2737
+ allOf:
2738
+ - $ref: '#/components/schemas/RetiredBundleVersionRef'
2739
+ - type: object
2740
+ required: [label, features, quotas, monthlyNet, yearlyNet]
2741
+ properties:
2742
+ label: { type: string }
2743
+ features: { type: array, items: { type: string } }
2744
+ quotas: { type: object, additionalProperties: { type: number } }
2745
+ monthlyNet: { type: [number, 'null'] }
2746
+ yearlyNet: { type: [number, 'null'] }
2747
+
2748
+ BundleRetirementPreview:
2749
+ type: object
2750
+ required: [retired, replacement, changes, asOf, reached, skipped, blockers]
2751
+ properties:
2752
+ retired: { $ref: '#/components/schemas/BundleVersionSide' }
2753
+ replacement: { $ref: '#/components/schemas/BundleVersionSide' }
2754
+ changes:
2755
+ type: array
2756
+ description: 'Every difference at the two versions own prices'
2757
+ items: { type: object }
2758
+ asOf: { type: string, description: 'ISO-8601: the moment it is computed for' }
2759
+ reached:
2760
+ type: array
2761
+ items:
2762
+ type: object
2763
+ required:
2764
+ [
2765
+ tenantId,
2766
+ subscriptionId,
2767
+ subscriptionBundleId,
2768
+ planKey,
2769
+ planCycle,
2770
+ billingCycle,
2771
+ effectiveAt,
2772
+ lastDayToCancel,
2773
+ reachedRecently,
2774
+ ]
2775
+ properties:
2776
+ tenantId: { type: string }
2777
+ subscriptionId: { type: string }
2778
+ subscriptionBundleId: { type: string }
2779
+ planKey: { type: string, description: 'The plan beside it at the date' }
2780
+ planCycle: { type: string }
2781
+ billingCycle: { type: string, description: 'The booking rhythm' }
2782
+ effectiveAt:
2783
+ { type: string, description: 'ISO-8601: an end of its period' }
2784
+ lastDayToCancel:
2785
+ { type: string, description: 'ISO date, UTC: the day before' }
2786
+ reachedRecently:
2787
+ {
2788
+ type: boolean,
2789
+ description: 'Its subscription was reached by a retirement within twelve months',
2790
+ }
2791
+ skipped:
2792
+ type: array
2793
+ items:
2794
+ type: object
2795
+ required: [tenantId, subscriptionId, subscriptionBundleId, reason]
2796
+ properties:
2797
+ tenantId: { type: string }
2798
+ subscriptionId: { type: string }
2799
+ subscriptionBundleId: { type: string }
2800
+ reason:
2801
+ {
2802
+ type: string,
2803
+ enum:
2804
+ [
2805
+ ended,
2806
+ cancelled-before,
2807
+ changes-before,
2808
+ no-term,
2809
+ already-told,
2810
+ ],
2811
+ }
2812
+ blockers:
2813
+ type: array
2814
+ description: 'Empty where it may be announced as it stands'
2815
+ items:
2816
+ type: object
2817
+ required: [code, message, params]
2818
+ properties:
2819
+ code: { type: string, description: 'BUNDLE_RETIREMENT_*, RETIREMENT_*' }
2820
+ message: { type: string }
2821
+ params: { type: object }
2822
+
2823
+ BundleRetirementAnnouncement:
2824
+ type: object
2825
+ required: [replacementBundleVersionId, subscriptionBundleIds]
2826
+ properties:
2827
+ replacementBundleVersionId: { type: string, format: uuid }
2828
+ subscriptionBundleIds:
2829
+ type: array
2830
+ description: 'The bookings the operator was shown in the preview'
2831
+ items: { type: string }
2832
+
2833
+ BundleVersionRetirementRecord:
2834
+ type: object
2835
+ required: [id, retired, replacement, announcedAt, announcedBy]
2836
+ properties:
2837
+ id: { type: string }
2838
+ retired: { $ref: '#/components/schemas/RetiredBundleVersionRef' }
2839
+ replacement: { $ref: '#/components/schemas/RetiredBundleVersionRef' }
2840
+ announcedAt: { type: string }
2841
+ announcedBy: { type: string }
2842
+
2843
+ BundleVersionRetirement:
2844
+ description: 'An add-on announcement as the list answers it, with how far it has come'
2845
+ allOf:
2846
+ - $ref: '#/components/schemas/BundleVersionRetirementRecord'
2847
+ - type: object
2848
+ required: [progress]
2849
+ properties:
2850
+ progress:
2851
+ type: object
2852
+ description: 'Counted over the bookings it reached'
2853
+ required: [moved, waiting, overdue, ended, notTold, reminded]
2854
+ properties:
2855
+ moved: { type: integer }
2856
+ waiting: { type: integer }
2857
+ overdue: { type: integer }
2858
+ ended: { type: integer }
2859
+ notTold: { type: integer }
2860
+ reminded: { type: integer }
2861
+
2862
+ BundleRetirementAnnounced:
2863
+ type: object
2864
+ required: [retirement, told, failed]
2865
+ properties:
2866
+ retirement: { $ref: '#/components/schemas/BundleVersionRetirementRecord' }
2867
+ told: { type: integer, description: 'Notices sent at once' }
2868
+ failed: { type: integer, description: 'Notices the next run sends' }
2869
+
2117
2870
  CatalogMarketingProjectionUpdate:
2118
2871
  type: object
2119
2872
  description: 'Partial update of a marketing projection'
@@ -2432,6 +3185,139 @@ components:
2432
3185
  contract was concluded; null where that line
2433
3186
  cannot be read.
2434
3187
 
3188
+ SubscriberReadiness:
3189
+ type: object
3190
+ description: >-
3191
+ Whether a subscriber can be given its next contract where a tax
3192
+ adapter decides: the empty fields of the address an invoice
3193
+ names, and the adapter's sentence where it supports no
3194
+ treatment for the subscriber as it stands.
3195
+ required: [ready, missing, taxRefusal]
3196
+ properties:
3197
+ ready: { type: boolean }
3198
+ missing:
3199
+ type: array
3200
+ items:
3201
+ type: string
3202
+ enum: [addressLine1, postalCode, city, country]
3203
+ taxRefusal:
3204
+ type: string
3205
+ nullable: true
3206
+
3207
+ AdminTenantSubscriber:
3208
+ type: object
3209
+ required: [subscriber, readiness]
3210
+ properties:
3211
+ subscriber:
3212
+ type: object
3213
+ nullable: true
3214
+ description: Null for a tenant that has no subscriber.
3215
+ required:
3216
+ [
3217
+ id,
3218
+ customerNumber,
3219
+ legalName,
3220
+ addressLine1,
3221
+ addressLine2,
3222
+ postalCode,
3223
+ city,
3224
+ country,
3225
+ business,
3226
+ vatId,
3227
+ vatIdValidated,
3228
+ taxNumber,
3229
+ migrated,
3230
+ ]
3231
+ properties:
3232
+ id: { type: string }
3233
+ customerNumber: { type: string }
3234
+ legalName: { type: string }
3235
+ addressLine1: { type: string, nullable: true }
3236
+ addressLine2: { type: string, nullable: true }
3237
+ postalCode: { type: string, nullable: true }
3238
+ city: { type: string, nullable: true }
3239
+ country: { type: string, nullable: true }
3240
+ business: { type: boolean, nullable: true }
3241
+ vatId: { type: string, nullable: true }
3242
+ vatIdValidated:
3243
+ type: boolean
3244
+ description: Whether the check that counts for the VAT id found it valid.
3245
+ taxNumber: { type: string, nullable: true }
3246
+ migrated:
3247
+ type: boolean
3248
+ description: Created from the application's own tenant record.
3249
+ readiness:
3250
+ nullable: true
3251
+ description: Null where no tax adapter decides, or the tenant has no subscriber.
3252
+ allOf:
3253
+ - $ref: '#/components/schemas/SubscriberReadiness'
3254
+
3255
+ AdminVatIdCheckOutcome:
3256
+ description: >-
3257
+ A check of a VAT number as the operator is shown it — what the
3258
+ service found, or why the check did not complete.
3259
+ oneOf:
3260
+ - type: object
3261
+ required: [completed, valid, checkedAt, service, counts]
3262
+ properties:
3263
+ completed: { type: boolean, enum: [true] }
3264
+ valid: { type: boolean }
3265
+ checkedAt: { type: string, format: date-time }
3266
+ service: { type: string }
3267
+ counts:
3268
+ type: boolean
3269
+ description: Whether it is the check that counts for the number held now.
3270
+ - type: object
3271
+ required: [completed, reason]
3272
+ properties:
3273
+ completed: { type: boolean, enum: [false] }
3274
+ reason: { type: string }
3275
+
3276
+ AdminSubscriberCorrected:
3277
+ type: object
3278
+ required: [subscriber, vatIdCheck]
3279
+ properties:
3280
+ subscriber: { $ref: '#/components/schemas/AdminTenantSubscriber' }
3281
+ vatIdCheck:
3282
+ nullable: true
3283
+ description: The check a VAT number got, where one was made.
3284
+ allOf:
3285
+ - $ref: '#/components/schemas/AdminVatIdCheckOutcome'
3286
+
3287
+ AdminSubscriberHistoryEntry:
3288
+ description: One correction, change of the tax origin, or check of the VAT number.
3289
+ oneOf:
3290
+ - type: object
3291
+ required: [kind, at, by, previous, corrected, reason]
3292
+ properties:
3293
+ kind: { type: string, enum: [identity-corrected] }
3294
+ at: { type: string, format: date-time }
3295
+ by: { type: string }
3296
+ previous: { type: object, additionalProperties: true }
3297
+ corrected: { type: object, additionalProperties: true }
3298
+ reason: { type: string }
3299
+ - type: object
3300
+ required: [kind, at, by, previous, changed, reason]
3301
+ properties:
3302
+ kind: { type: string, enum: [tax-origin-changed] }
3303
+ at: { type: string, format: date-time }
3304
+ by: { type: string }
3305
+ previous: { type: object, additionalProperties: true }
3306
+ changed: { type: object, additionalProperties: true }
3307
+ reason:
3308
+ type: string
3309
+ nullable: true
3310
+ description: Null for a change of the country with the contact details.
3311
+ - type: object
3312
+ required: [kind, at, vatId, valid, service, counts]
3313
+ properties:
3314
+ kind: { type: string, enum: [vat-id-checked] }
3315
+ at: { type: string, format: date-time }
3316
+ vatId: { type: string }
3317
+ valid: { type: boolean }
3318
+ service: { type: string }
3319
+ counts: { type: boolean }
3320
+
2435
3321
  # ────────── User ──────────
2436
3322
  AdminUserRow:
2437
3323
  type: object