@saasicat/spec 1.0.0-rc.23 → 1.0.0-rc.25
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/acceptance/README.md +1 -2
- package/acceptance/plan-version/publish-does-not-touch-existing-contracts.yaml +2 -9
- package/admin-api.openapi.yaml +764 -11
- package/package.json +1 -1
- package/prisma-fragments/01-subscription.prisma +10 -17
- package/prisma-fragments/03-plan-versions.prisma +1 -1
- package/prisma-fragments/04-audit-log.prisma +2 -1
- package/prisma-fragments/05-bundle.prisma +4 -3
- package/prisma-fragments/08-subscription-contract.prisma +7 -0
- package/prisma-fragments/09-pending-registration.prisma +6 -0
- package/prisma-fragments/10-super-admin.prisma +1 -1
- package/prisma-fragments/11-subscription-bundle.prisma +12 -2
- package/prisma-fragments/13-subscriber.prisma +81 -8
- package/prisma-fragments/15-subscriber-ledger.prisma +1 -1
- package/prisma-fragments/17-subscription-notice.prisma +58 -0
- package/prisma-fragments/18-version-retirement.prisma +44 -0
- package/prisma-fragments/19-bundle-version-retirement.prisma +44 -0
- package/prisma-fragments/README.md +23 -19
- package/schemas/audit-event.schema.json +2 -2
- package/schemas/plan-catalog.schema.json +40 -4
- package/schemas/subscriber-ledger.schema.json +2 -1
- package/sql/1.0-a-correction-carries-its-order.postgres.sql +78 -0
- package/sql/1.0-a-newer-version-is-only-offered.postgres.sql +33 -0
- package/sql/1.0-a-retirement-is-announced.postgres.sql +43 -0
- package/sql/1.0-a-scheduled-change-keeps-its-quoted-version.postgres.sql +129 -0
- package/sql/1.0-a-sign-up-keeps-its-vat-id-check.postgres.sql +38 -0
- package/sql/1.0-a-subscriber-has-a-tax-origin.postgres.sql +113 -0
- package/sql/1.0-a-subscriber-is-told-once.postgres.sql +48 -0
- package/sql/1.0-an-add-on-retirement-is-announced.postgres.sql +44 -0
- package/sql/1.0-an-add-on-switch-waits-for-its-term.postgres.sql +52 -0
- package/sql/constraints.postgres.sql +10 -0
- package/sql/reference-schema.postgres.sql +137 -10
package/admin-api.openapi.yaml
CHANGED
|
@@ -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.
|
|
32
|
+
version: 1.0.0-rc.25
|
|
33
33
|
description: |
|
|
34
34
|
Read and write operations for platform administration:
|
|
35
35
|
tenants, users, subscriptions, promo codes, audit log,
|
|
@@ -352,11 +352,7 @@ paths:
|
|
|
352
352
|
properties:
|
|
353
353
|
planId: { type: string }
|
|
354
354
|
billingCycle: { type: string, enum: [MONTHLY, YEARLY] }
|
|
355
|
-
customLimits:
|
|
356
|
-
type: object
|
|
357
|
-
additionalProperties:
|
|
358
|
-
type: integer
|
|
359
|
-
description: 'quotaKey → maxValue (-1 = unlimited)'
|
|
355
|
+
customLimits: { $ref: '#/components/schemas/CustomLimits' }
|
|
360
356
|
customMonthlyNet: { type: number, format: decimal }
|
|
361
357
|
isPilot: { type: boolean }
|
|
362
358
|
pilotEndsAt: { type: string, format: date-time }
|
|
@@ -391,6 +387,201 @@ paths:
|
|
|
391
387
|
'404':
|
|
392
388
|
description: No tenant with this slug (`TENANT_NOT_FOUND`)
|
|
393
389
|
|
|
390
|
+
/tenants/{slug}/subscriber:
|
|
391
|
+
parameters:
|
|
392
|
+
- $ref: '#/components/parameters/TenantSlug'
|
|
393
|
+
get:
|
|
394
|
+
tags: [subscriptions]
|
|
395
|
+
summary: The tenant's subscriber, its tax details and what holds its next contract back
|
|
396
|
+
description: >-
|
|
397
|
+
Served where the platform keeps subscribers and shows tenants
|
|
398
|
+
(`adminResources`); the manifest announces it as
|
|
399
|
+
`subscribers.read`. The standing is computed when it is read,
|
|
400
|
+
and only where a tax adapter decides; elsewhere `readiness` is
|
|
401
|
+
null.
|
|
402
|
+
responses:
|
|
403
|
+
'200':
|
|
404
|
+
description: The subscriber, or none
|
|
405
|
+
content:
|
|
406
|
+
application/json:
|
|
407
|
+
schema: { $ref: '#/components/schemas/AdminTenantSubscriber' }
|
|
408
|
+
'404':
|
|
409
|
+
description: No tenant with this slug (`TENANT_NOT_FOUND`)
|
|
410
|
+
|
|
411
|
+
/tenants/{slug}/subscriber/history:
|
|
412
|
+
parameters:
|
|
413
|
+
- $ref: '#/components/parameters/TenantSlug'
|
|
414
|
+
get:
|
|
415
|
+
tags: [subscriptions]
|
|
416
|
+
summary: Every correction, change of country or business status, and check of the subscriber, the latest first
|
|
417
|
+
description: >-
|
|
418
|
+
Served beside `/tenants/{slug}/subscriber`; the manifest
|
|
419
|
+
announces it as `subscribers.correct`. A correction that gives
|
|
420
|
+
another VAT number is named once, as the correction it was. Empty
|
|
421
|
+
for a tenant without a subscriber.
|
|
422
|
+
responses:
|
|
423
|
+
'200':
|
|
424
|
+
description: The history
|
|
425
|
+
content:
|
|
426
|
+
application/json:
|
|
427
|
+
schema:
|
|
428
|
+
type: object
|
|
429
|
+
required: [entries]
|
|
430
|
+
properties:
|
|
431
|
+
entries:
|
|
432
|
+
type: array
|
|
433
|
+
items:
|
|
434
|
+
$ref: '#/components/schemas/AdminSubscriberHistoryEntry'
|
|
435
|
+
'404':
|
|
436
|
+
description: No tenant with this slug (`TENANT_NOT_FOUND`)
|
|
437
|
+
|
|
438
|
+
/tenants/{slug}/subscriber/identity:
|
|
439
|
+
parameters:
|
|
440
|
+
- $ref: '#/components/parameters/TenantSlug'
|
|
441
|
+
post:
|
|
442
|
+
tags: [subscriptions]
|
|
443
|
+
summary: Correct the legal identity of the tenant's subscriber
|
|
444
|
+
description: >-
|
|
445
|
+
A correction of the same legal entity — a field left out keeps
|
|
446
|
+
its value, `null` clears a tax identifier — recorded with the
|
|
447
|
+
values it replaced, the reason and who made it. `kind` is the
|
|
448
|
+
operator's declaration: a takeover by another entity is refused
|
|
449
|
+
(`SUBSCRIBER_TAKEOVER_IS_A_TRANSFER`). Where a tax adapter
|
|
450
|
+
decides and the correction gives another VAT number, that
|
|
451
|
+
number is checked right after and the check recorded, whatever
|
|
452
|
+
it found; the answer says how it went.
|
|
453
|
+
x-mfa-required: true
|
|
454
|
+
requestBody:
|
|
455
|
+
required: true
|
|
456
|
+
content:
|
|
457
|
+
application/json:
|
|
458
|
+
schema:
|
|
459
|
+
type: object
|
|
460
|
+
required: [kind, reason]
|
|
461
|
+
properties:
|
|
462
|
+
kind: { type: string, enum: [correction, takeover] }
|
|
463
|
+
legalName: { type: string }
|
|
464
|
+
vatId: { type: string, nullable: true }
|
|
465
|
+
taxNumber: { type: string, nullable: true }
|
|
466
|
+
reason: { type: string, maxLength: 500 }
|
|
467
|
+
responses:
|
|
468
|
+
'200':
|
|
469
|
+
description: The subscriber as it now stands, and the check its new number got
|
|
470
|
+
content:
|
|
471
|
+
application/json:
|
|
472
|
+
schema: { $ref: '#/components/schemas/AdminSubscriberCorrected' }
|
|
473
|
+
'404':
|
|
474
|
+
description: No tenant with this slug (`TENANT_NOT_FOUND`)
|
|
475
|
+
'409':
|
|
476
|
+
description: The tenant has no subscriber (`SUBSCRIBER_REQUIRED`)
|
|
477
|
+
'422':
|
|
478
|
+
description: >-
|
|
479
|
+
A takeover, no reason, or a correction that changes
|
|
480
|
+
nothing (`SUBSCRIBER_TAKEOVER_IS_A_TRANSFER`,
|
|
481
|
+
`SUBSCRIBER_CORRECTION_REASON_REQUIRED`,
|
|
482
|
+
`SUBSCRIBER_CORRECTION_CHANGES_NOTHING`)
|
|
483
|
+
|
|
484
|
+
/tenants/{slug}/subscriber/business-status:
|
|
485
|
+
parameters:
|
|
486
|
+
- $ref: '#/components/parameters/TenantSlug'
|
|
487
|
+
post:
|
|
488
|
+
tags: [subscriptions]
|
|
489
|
+
summary: Record whether the tenant's subscriber acts as a business, and why
|
|
490
|
+
description: >-
|
|
491
|
+
Recorded as a change of the tax origin with its reason and who
|
|
492
|
+
made it; it applies from the next contract. Setting the status it
|
|
493
|
+
has records nothing.
|
|
494
|
+
x-mfa-required: true
|
|
495
|
+
requestBody:
|
|
496
|
+
required: true
|
|
497
|
+
content:
|
|
498
|
+
application/json:
|
|
499
|
+
schema:
|
|
500
|
+
type: object
|
|
501
|
+
required: [business, reason]
|
|
502
|
+
properties:
|
|
503
|
+
business: { type: boolean, nullable: true }
|
|
504
|
+
reason: { type: string, maxLength: 500 }
|
|
505
|
+
responses:
|
|
506
|
+
'200':
|
|
507
|
+
description: The subscriber as it now stands
|
|
508
|
+
content:
|
|
509
|
+
application/json:
|
|
510
|
+
schema: { $ref: '#/components/schemas/AdminSubscriberCorrected' }
|
|
511
|
+
'404':
|
|
512
|
+
description: No tenant with this slug (`TENANT_NOT_FOUND`)
|
|
513
|
+
'409':
|
|
514
|
+
description: The tenant has no subscriber (`SUBSCRIBER_REQUIRED`)
|
|
515
|
+
'422':
|
|
516
|
+
description: No reason (`SUBSCRIBER_BUSINESS_STATUS_REASON_REQUIRED`)
|
|
517
|
+
|
|
518
|
+
/tenants/{slug}/subscriber/vat-id-check:
|
|
519
|
+
parameters:
|
|
520
|
+
- $ref: '#/components/parameters/TenantSlug'
|
|
521
|
+
post:
|
|
522
|
+
tags: [subscriptions]
|
|
523
|
+
summary: Check the VAT number the tenant's subscriber holds, again
|
|
524
|
+
description: >-
|
|
525
|
+
Asks the service the tax adapter names and records a completed
|
|
526
|
+
check, valid or not; a check that does not complete records
|
|
527
|
+
nothing and says why. No second factor: nothing is entered, and
|
|
528
|
+
the answer is the service's.
|
|
529
|
+
responses:
|
|
530
|
+
'200':
|
|
531
|
+
description: The subscriber as it now stands, and what the check found
|
|
532
|
+
content:
|
|
533
|
+
application/json:
|
|
534
|
+
schema: { $ref: '#/components/schemas/AdminSubscriberCorrected' }
|
|
535
|
+
'404':
|
|
536
|
+
description: No tenant with this slug (`TENANT_NOT_FOUND`)
|
|
537
|
+
'409':
|
|
538
|
+
description: >-
|
|
539
|
+
The tenant has no subscriber, or no tax adapter names a
|
|
540
|
+
service (`SUBSCRIBER_REQUIRED`, `TAX_VAT_ID_CHECK_NOT_AVAILABLE`)
|
|
541
|
+
'422':
|
|
542
|
+
description: The subscriber holds no VAT number (`SUBSCRIBER_VAT_ID_MISSING`)
|
|
543
|
+
|
|
544
|
+
/subscribers/attention:
|
|
545
|
+
get:
|
|
546
|
+
tags: [subscriptions]
|
|
547
|
+
summary: Which of these tenants hold their subscriber back from its next contract
|
|
548
|
+
description: >-
|
|
549
|
+
Served beside `/tenants/{slug}/subscriber`; the manifest
|
|
550
|
+
announces `subscribers.attention` only where a tax adapter
|
|
551
|
+
decides, and without one the answer is always empty. A tenant
|
|
552
|
+
whose subscriber is ready, and one without a subscriber, are
|
|
553
|
+
left out.
|
|
554
|
+
parameters:
|
|
555
|
+
- name: tenantId
|
|
556
|
+
in: query
|
|
557
|
+
description: The tenants of the page shown; repeat the parameter for each.
|
|
558
|
+
schema:
|
|
559
|
+
type: array
|
|
560
|
+
maxItems: 200
|
|
561
|
+
items: { type: string, minLength: 1 }
|
|
562
|
+
style: form
|
|
563
|
+
explode: true
|
|
564
|
+
responses:
|
|
565
|
+
'200':
|
|
566
|
+
description: The tenants held back, each with why
|
|
567
|
+
content:
|
|
568
|
+
application/json:
|
|
569
|
+
schema:
|
|
570
|
+
type: object
|
|
571
|
+
required: [attention]
|
|
572
|
+
properties:
|
|
573
|
+
attention:
|
|
574
|
+
type: array
|
|
575
|
+
items:
|
|
576
|
+
type: object
|
|
577
|
+
required: [tenantId, readiness]
|
|
578
|
+
properties:
|
|
579
|
+
tenantId: { type: string }
|
|
580
|
+
readiness:
|
|
581
|
+
$ref: '#/components/schemas/SubscriberReadiness'
|
|
582
|
+
'400':
|
|
583
|
+
description: More than 200 tenants, or an empty tenant id
|
|
584
|
+
|
|
394
585
|
# ────────────────────────────────────────────────────────
|
|
395
586
|
# Users
|
|
396
587
|
# ────────────────────────────────────────────────────────
|
|
@@ -816,8 +1007,9 @@ paths:
|
|
|
816
1007
|
id: { type: string }
|
|
817
1008
|
tenant:
|
|
818
1009
|
type: object
|
|
819
|
-
required: [slug, name]
|
|
1010
|
+
required: [id, slug, name]
|
|
820
1011
|
properties:
|
|
1012
|
+
id: { type: string }
|
|
821
1013
|
slug: { type: string }
|
|
822
1014
|
name: { type: string }
|
|
823
1015
|
plan: { type: string }
|
|
@@ -1000,7 +1192,7 @@ paths:
|
|
|
1000
1192
|
parameters: [{ name: id, in: path, required: true, schema: { type: string } }]
|
|
1001
1193
|
post:
|
|
1002
1194
|
tags: [catalog]
|
|
1003
|
-
summary:
|
|
1195
|
+
summary: End a plan version's sale (endsAt; without a successor version)
|
|
1004
1196
|
x-mfa-required: true
|
|
1005
1197
|
requestBody:
|
|
1006
1198
|
required: true
|
|
@@ -1014,6 +1206,154 @@ paths:
|
|
|
1014
1206
|
}
|
|
1015
1207
|
responses: { '200': { description: Terminated } }
|
|
1016
1208
|
|
|
1209
|
+
# ── Retiring a version for running subscriptions (packages/nest/src/billing/version-retirement*) ──
|
|
1210
|
+
#
|
|
1211
|
+
# Served where the manifest carries the capability `planVersions.retire`. The
|
|
1212
|
+
# subscriptions on a version no longer on sale move to a replacement on sale, each at
|
|
1213
|
+
# the first end of a term at least three calendar months after the announcement, and
|
|
1214
|
+
# may cancel without notice until then. Where the operator's terms are not confirmed to
|
|
1215
|
+
# allow it, both routes refuse with `RETIREMENT_TERMS_NOT_CONFIRMED` before reading anything.
|
|
1216
|
+
|
|
1217
|
+
/catalog/plan-versions/{id}/retirement:
|
|
1218
|
+
parameters: [{ name: id, in: path, required: true, schema: { type: string } }]
|
|
1219
|
+
get:
|
|
1220
|
+
tags: [catalog]
|
|
1221
|
+
summary: What retiring the version for its running subscriptions would do now
|
|
1222
|
+
parameters:
|
|
1223
|
+
- name: replacement
|
|
1224
|
+
in: query
|
|
1225
|
+
required: true
|
|
1226
|
+
description: The plan version the subscriptions continue on
|
|
1227
|
+
schema: { type: string, format: uuid }
|
|
1228
|
+
responses:
|
|
1229
|
+
'200':
|
|
1230
|
+
description: >-
|
|
1231
|
+
Whom it reaches and when, whom it does not and why, and every blocker the
|
|
1232
|
+
announcement would refuse with
|
|
1233
|
+
content:
|
|
1234
|
+
application/json:
|
|
1235
|
+
schema: { $ref: '#/components/schemas/RetirementPreview' }
|
|
1236
|
+
'404':
|
|
1237
|
+
description: Either version does not exist (`PLAN_VERSION_NOT_FOUND`)
|
|
1238
|
+
'422':
|
|
1239
|
+
description: >-
|
|
1240
|
+
The terms are not confirmed (`RETIREMENT_TERMS_NOT_CONFIRMED`), or the
|
|
1241
|
+
replacement is the version itself (`RETIREMENT_REPLACEMENT_IS_RETIRED`)
|
|
1242
|
+
post:
|
|
1243
|
+
tags: [catalog]
|
|
1244
|
+
summary: Announce the retirement to every subscription it reaches
|
|
1245
|
+
description: >-
|
|
1246
|
+
The announcement and one notice per subscription are recorded in one
|
|
1247
|
+
transaction, then handed to the application's `SubscriptionNoticePort`; a notice
|
|
1248
|
+
that cannot be sent now is sent by the next run. Audited as PLAN_VERSION_RETIRE.
|
|
1249
|
+
x-mfa-required: true
|
|
1250
|
+
requestBody:
|
|
1251
|
+
required: true
|
|
1252
|
+
content:
|
|
1253
|
+
application/json:
|
|
1254
|
+
schema: { $ref: '#/components/schemas/RetirementAnnouncement' }
|
|
1255
|
+
responses:
|
|
1256
|
+
'201':
|
|
1257
|
+
description: Announced
|
|
1258
|
+
content:
|
|
1259
|
+
application/json:
|
|
1260
|
+
schema: { $ref: '#/components/schemas/RetirementAnnounced' }
|
|
1261
|
+
'409':
|
|
1262
|
+
description: >-
|
|
1263
|
+
The subscriptions it reaches are no longer the ones named
|
|
1264
|
+
(`RETIREMENT_PREVIEW_CHANGED`); the body carries the preview as it stands
|
|
1265
|
+
'422':
|
|
1266
|
+
description: >-
|
|
1267
|
+
The first blocker the preview reports (`RETIREMENT_*`), with every blocker
|
|
1268
|
+
beside it
|
|
1269
|
+
|
|
1270
|
+
/catalog/version-retirements:
|
|
1271
|
+
get:
|
|
1272
|
+
tags: [catalog]
|
|
1273
|
+
summary: Every retirement announced, the most recent first
|
|
1274
|
+
responses:
|
|
1275
|
+
'200':
|
|
1276
|
+
description: List
|
|
1277
|
+
content:
|
|
1278
|
+
application/json:
|
|
1279
|
+
schema:
|
|
1280
|
+
type: array
|
|
1281
|
+
items: { $ref: '#/components/schemas/VersionRetirement' }
|
|
1282
|
+
|
|
1283
|
+
# Served where the manifest carries the capability `bundleVersions.retire`. The bookings
|
|
1284
|
+
# on an add-on version no longer on sale continue on a version of the same add-on on
|
|
1285
|
+
# sale, each at the first end of its own period at least three calendar months after
|
|
1286
|
+
# its notice reached the subscriber, and may be cancelled without their minimum term
|
|
1287
|
+
# until then. Where the operator's terms are not confirmed to allow it, both routes
|
|
1288
|
+
# refuse with `RETIREMENT_TERMS_NOT_CONFIRMED` before reading anything.
|
|
1289
|
+
|
|
1290
|
+
/catalog/bundle-versions/{id}/retirement:
|
|
1291
|
+
parameters: [{ name: id, in: path, required: true, schema: { type: string } }]
|
|
1292
|
+
get:
|
|
1293
|
+
tags: [catalog]
|
|
1294
|
+
summary: What retiring the add-on version for its running bookings would do now
|
|
1295
|
+
parameters:
|
|
1296
|
+
- name: replacement
|
|
1297
|
+
in: query
|
|
1298
|
+
required: true
|
|
1299
|
+
description: The version of the same add-on the bookings continue on
|
|
1300
|
+
schema: { type: string, format: uuid }
|
|
1301
|
+
responses:
|
|
1302
|
+
'200':
|
|
1303
|
+
description: >-
|
|
1304
|
+
Which bookings it reaches and when, which it does not and why, and every
|
|
1305
|
+
blocker the announcement would refuse with
|
|
1306
|
+
content:
|
|
1307
|
+
application/json:
|
|
1308
|
+
schema: { $ref: '#/components/schemas/BundleRetirementPreview' }
|
|
1309
|
+
'404':
|
|
1310
|
+
description: Either version does not exist (`BUNDLE_VERSION_NOT_FOUND`)
|
|
1311
|
+
'422':
|
|
1312
|
+
description: >-
|
|
1313
|
+
The terms are not confirmed (`RETIREMENT_TERMS_NOT_CONFIRMED`), or the
|
|
1314
|
+
replacement is the version itself (`RETIREMENT_REPLACEMENT_IS_RETIRED`)
|
|
1315
|
+
post:
|
|
1316
|
+
tags: [catalog]
|
|
1317
|
+
summary: Announce the add-on retirement to every booking it reaches
|
|
1318
|
+
description: >-
|
|
1319
|
+
The announcement and one notice per booking are recorded in one transaction,
|
|
1320
|
+
then handed to the application's `SubscriptionNoticePort` as
|
|
1321
|
+
`bundle-version-retired`; a notice that cannot be sent now is sent by the next
|
|
1322
|
+
run. Audited as BUNDLE_VERSION_RETIRE.
|
|
1323
|
+
x-mfa-required: true
|
|
1324
|
+
requestBody:
|
|
1325
|
+
required: true
|
|
1326
|
+
content:
|
|
1327
|
+
application/json:
|
|
1328
|
+
schema: { $ref: '#/components/schemas/BundleRetirementAnnouncement' }
|
|
1329
|
+
responses:
|
|
1330
|
+
'201':
|
|
1331
|
+
description: Announced
|
|
1332
|
+
content:
|
|
1333
|
+
application/json:
|
|
1334
|
+
schema: { $ref: '#/components/schemas/BundleRetirementAnnounced' }
|
|
1335
|
+
'409':
|
|
1336
|
+
description: >-
|
|
1337
|
+
The bookings it reaches are no longer the ones named
|
|
1338
|
+
(`RETIREMENT_PREVIEW_CHANGED`); the body carries the preview as it stands
|
|
1339
|
+
'422':
|
|
1340
|
+
description: >-
|
|
1341
|
+
The first blocker the preview reports (`BUNDLE_RETIREMENT_*`,
|
|
1342
|
+
`RETIREMENT_WITHIN_TWELVE_MONTHS`), with every blocker beside it
|
|
1343
|
+
|
|
1344
|
+
/catalog/bundle-version-retirements:
|
|
1345
|
+
get:
|
|
1346
|
+
tags: [catalog]
|
|
1347
|
+
summary: Every add-on retirement announced, the most recent first
|
|
1348
|
+
responses:
|
|
1349
|
+
'200':
|
|
1350
|
+
description: List, each with how far it has come over the bookings it reached
|
|
1351
|
+
content:
|
|
1352
|
+
application/json:
|
|
1353
|
+
schema:
|
|
1354
|
+
type: array
|
|
1355
|
+
items: { $ref: '#/components/schemas/BundleVersionRetirement' }
|
|
1356
|
+
|
|
1017
1357
|
# ── Bundles ──
|
|
1018
1358
|
/catalog/bundles:
|
|
1019
1359
|
get:
|
|
@@ -1925,6 +2265,270 @@ components:
|
|
|
1925
2265
|
properties:
|
|
1926
2266
|
endsAt: { type: string, description: 'ISO-8601; must lie in the future' }
|
|
1927
2267
|
|
|
2268
|
+
RetiredVersionRef:
|
|
2269
|
+
type: object
|
|
2270
|
+
required: [planVersionId, planKey, version]
|
|
2271
|
+
properties:
|
|
2272
|
+
planVersionId: { type: string }
|
|
2273
|
+
planKey: { type: string }
|
|
2274
|
+
version: { type: integer }
|
|
2275
|
+
|
|
2276
|
+
RetirementSide:
|
|
2277
|
+
description: 'A version as the subscribers compare it; a price of null is a rhythm it is not sold in'
|
|
2278
|
+
allOf:
|
|
2279
|
+
- $ref: '#/components/schemas/RetiredVersionRef'
|
|
2280
|
+
- type: object
|
|
2281
|
+
required: [features, quotas, monthlyNet, yearlyNet, validUntil, endsAt]
|
|
2282
|
+
properties:
|
|
2283
|
+
features: { type: array, items: { type: string } }
|
|
2284
|
+
quotas: { type: object, additionalProperties: { type: number } }
|
|
2285
|
+
monthlyNet: { type: [number, 'null'] }
|
|
2286
|
+
yearlyNet: { type: [number, 'null'] }
|
|
2287
|
+
validUntil: { type: [string, 'null'] }
|
|
2288
|
+
endsAt: { type: [string, 'null'] }
|
|
2289
|
+
|
|
2290
|
+
RetirementPreview:
|
|
2291
|
+
type: object
|
|
2292
|
+
required: [retired, replacement, changes, asOf, reached, skipped, blockers]
|
|
2293
|
+
properties:
|
|
2294
|
+
retired: { $ref: '#/components/schemas/RetirementSide' }
|
|
2295
|
+
replacement: { $ref: '#/components/schemas/RetirementSide' }
|
|
2296
|
+
changes:
|
|
2297
|
+
type: array
|
|
2298
|
+
description: 'Every difference, retired to replacement, as the catalogue diff states it'
|
|
2299
|
+
items: { type: object }
|
|
2300
|
+
asOf: { type: string, description: 'ISO-8601: the moment it is computed for' }
|
|
2301
|
+
reached:
|
|
2302
|
+
type: array
|
|
2303
|
+
items:
|
|
2304
|
+
type: object
|
|
2305
|
+
required:
|
|
2306
|
+
[
|
|
2307
|
+
tenantId,
|
|
2308
|
+
subscriptionId,
|
|
2309
|
+
status,
|
|
2310
|
+
billingCycle,
|
|
2311
|
+
effectiveAt,
|
|
2312
|
+
lastDayToCancel,
|
|
2313
|
+
reachedRecently,
|
|
2314
|
+
]
|
|
2315
|
+
properties:
|
|
2316
|
+
tenantId: { type: string }
|
|
2317
|
+
subscriptionId: { type: string }
|
|
2318
|
+
status: { type: string }
|
|
2319
|
+
billingCycle: { type: string }
|
|
2320
|
+
effectiveAt: { type: string, description: 'ISO-8601: a term end' }
|
|
2321
|
+
lastDayToCancel:
|
|
2322
|
+
{ type: string, description: 'ISO date, UTC: the day before' }
|
|
2323
|
+
reachedRecently:
|
|
2324
|
+
{
|
|
2325
|
+
type: boolean,
|
|
2326
|
+
description: 'Reached by a retirement within twelve months',
|
|
2327
|
+
}
|
|
2328
|
+
skipped:
|
|
2329
|
+
type: array
|
|
2330
|
+
items:
|
|
2331
|
+
type: object
|
|
2332
|
+
required: [tenantId, subscriptionId, reason]
|
|
2333
|
+
properties:
|
|
2334
|
+
tenantId: { type: string }
|
|
2335
|
+
subscriptionId: { type: string }
|
|
2336
|
+
reason:
|
|
2337
|
+
{
|
|
2338
|
+
type: string,
|
|
2339
|
+
enum:
|
|
2340
|
+
[
|
|
2341
|
+
ended,
|
|
2342
|
+
cancelled-before,
|
|
2343
|
+
changes-before,
|
|
2344
|
+
no-term,
|
|
2345
|
+
already-told,
|
|
2346
|
+
],
|
|
2347
|
+
}
|
|
2348
|
+
blockers:
|
|
2349
|
+
type: array
|
|
2350
|
+
description: 'Empty where it may be announced as it stands'
|
|
2351
|
+
items:
|
|
2352
|
+
type: object
|
|
2353
|
+
required: [code, message, params]
|
|
2354
|
+
properties:
|
|
2355
|
+
code: { type: string, description: 'RETIREMENT_*' }
|
|
2356
|
+
message: { type: string }
|
|
2357
|
+
params: { type: object }
|
|
2358
|
+
|
|
2359
|
+
RetirementAnnouncement:
|
|
2360
|
+
type: object
|
|
2361
|
+
required: [replacementPlanVersionId, subscriptionIds]
|
|
2362
|
+
properties:
|
|
2363
|
+
replacementPlanVersionId: { type: string, format: uuid }
|
|
2364
|
+
subscriptionIds:
|
|
2365
|
+
type: array
|
|
2366
|
+
description: 'The subscriptions the operator was shown in the preview'
|
|
2367
|
+
items: { type: string }
|
|
2368
|
+
|
|
2369
|
+
VersionRetirement:
|
|
2370
|
+
type: object
|
|
2371
|
+
required: [id, retired, replacement, announcedAt, announcedBy]
|
|
2372
|
+
properties:
|
|
2373
|
+
id: { type: string }
|
|
2374
|
+
retired: { $ref: '#/components/schemas/RetiredVersionRef' }
|
|
2375
|
+
replacement: { $ref: '#/components/schemas/RetiredVersionRef' }
|
|
2376
|
+
announcedAt: { type: string }
|
|
2377
|
+
announcedBy: { type: string }
|
|
2378
|
+
|
|
2379
|
+
RetirementAnnounced:
|
|
2380
|
+
type: object
|
|
2381
|
+
required: [retirement, told, failed]
|
|
2382
|
+
properties:
|
|
2383
|
+
retirement: { $ref: '#/components/schemas/VersionRetirement' }
|
|
2384
|
+
told: { type: integer, description: 'Notices sent at once' }
|
|
2385
|
+
failed: { type: integer, description: 'Notices the next run sends' }
|
|
2386
|
+
|
|
2387
|
+
RetiredBundleVersionRef:
|
|
2388
|
+
type: object
|
|
2389
|
+
required: [bundleVersionId, bundleKey, version]
|
|
2390
|
+
properties:
|
|
2391
|
+
bundleVersionId: { type: string }
|
|
2392
|
+
bundleKey: { type: string }
|
|
2393
|
+
version: { type: integer }
|
|
2394
|
+
|
|
2395
|
+
BundleVersionSide:
|
|
2396
|
+
description: >-
|
|
2397
|
+
An add-on version as the subscribers compare it, at its own prices; each booking
|
|
2398
|
+
is told the prices beside its plan. A price of null is a rhythm it is not sold in
|
|
2399
|
+
allOf:
|
|
2400
|
+
- $ref: '#/components/schemas/RetiredBundleVersionRef'
|
|
2401
|
+
- type: object
|
|
2402
|
+
required: [label, features, quotas, monthlyNet, yearlyNet]
|
|
2403
|
+
properties:
|
|
2404
|
+
label: { type: string }
|
|
2405
|
+
features: { type: array, items: { type: string } }
|
|
2406
|
+
quotas: { type: object, additionalProperties: { type: number } }
|
|
2407
|
+
monthlyNet: { type: [number, 'null'] }
|
|
2408
|
+
yearlyNet: { type: [number, 'null'] }
|
|
2409
|
+
|
|
2410
|
+
BundleRetirementPreview:
|
|
2411
|
+
type: object
|
|
2412
|
+
required: [retired, replacement, changes, asOf, reached, skipped, blockers]
|
|
2413
|
+
properties:
|
|
2414
|
+
retired: { $ref: '#/components/schemas/BundleVersionSide' }
|
|
2415
|
+
replacement: { $ref: '#/components/schemas/BundleVersionSide' }
|
|
2416
|
+
changes:
|
|
2417
|
+
type: array
|
|
2418
|
+
description: 'Every difference at the two versions own prices'
|
|
2419
|
+
items: { type: object }
|
|
2420
|
+
asOf: { type: string, description: 'ISO-8601: the moment it is computed for' }
|
|
2421
|
+
reached:
|
|
2422
|
+
type: array
|
|
2423
|
+
items:
|
|
2424
|
+
type: object
|
|
2425
|
+
required:
|
|
2426
|
+
[
|
|
2427
|
+
tenantId,
|
|
2428
|
+
subscriptionId,
|
|
2429
|
+
subscriptionBundleId,
|
|
2430
|
+
planKey,
|
|
2431
|
+
planCycle,
|
|
2432
|
+
billingCycle,
|
|
2433
|
+
effectiveAt,
|
|
2434
|
+
lastDayToCancel,
|
|
2435
|
+
reachedRecently,
|
|
2436
|
+
]
|
|
2437
|
+
properties:
|
|
2438
|
+
tenantId: { type: string }
|
|
2439
|
+
subscriptionId: { type: string }
|
|
2440
|
+
subscriptionBundleId: { type: string }
|
|
2441
|
+
planKey: { type: string, description: 'The plan beside it at the date' }
|
|
2442
|
+
planCycle: { type: string }
|
|
2443
|
+
billingCycle: { type: string, description: 'The booking rhythm' }
|
|
2444
|
+
effectiveAt:
|
|
2445
|
+
{ type: string, description: 'ISO-8601: an end of its period' }
|
|
2446
|
+
lastDayToCancel:
|
|
2447
|
+
{ type: string, description: 'ISO date, UTC: the day before' }
|
|
2448
|
+
reachedRecently:
|
|
2449
|
+
{
|
|
2450
|
+
type: boolean,
|
|
2451
|
+
description: 'Its subscription was reached by a retirement within twelve months',
|
|
2452
|
+
}
|
|
2453
|
+
skipped:
|
|
2454
|
+
type: array
|
|
2455
|
+
items:
|
|
2456
|
+
type: object
|
|
2457
|
+
required: [tenantId, subscriptionId, subscriptionBundleId, reason]
|
|
2458
|
+
properties:
|
|
2459
|
+
tenantId: { type: string }
|
|
2460
|
+
subscriptionId: { type: string }
|
|
2461
|
+
subscriptionBundleId: { type: string }
|
|
2462
|
+
reason:
|
|
2463
|
+
{
|
|
2464
|
+
type: string,
|
|
2465
|
+
enum:
|
|
2466
|
+
[
|
|
2467
|
+
ended,
|
|
2468
|
+
cancelled-before,
|
|
2469
|
+
changes-before,
|
|
2470
|
+
no-term,
|
|
2471
|
+
already-told,
|
|
2472
|
+
],
|
|
2473
|
+
}
|
|
2474
|
+
blockers:
|
|
2475
|
+
type: array
|
|
2476
|
+
description: 'Empty where it may be announced as it stands'
|
|
2477
|
+
items:
|
|
2478
|
+
type: object
|
|
2479
|
+
required: [code, message, params]
|
|
2480
|
+
properties:
|
|
2481
|
+
code: { type: string, description: 'BUNDLE_RETIREMENT_*, RETIREMENT_*' }
|
|
2482
|
+
message: { type: string }
|
|
2483
|
+
params: { type: object }
|
|
2484
|
+
|
|
2485
|
+
BundleRetirementAnnouncement:
|
|
2486
|
+
type: object
|
|
2487
|
+
required: [replacementBundleVersionId, subscriptionBundleIds]
|
|
2488
|
+
properties:
|
|
2489
|
+
replacementBundleVersionId: { type: string, format: uuid }
|
|
2490
|
+
subscriptionBundleIds:
|
|
2491
|
+
type: array
|
|
2492
|
+
description: 'The bookings the operator was shown in the preview'
|
|
2493
|
+
items: { type: string }
|
|
2494
|
+
|
|
2495
|
+
BundleVersionRetirementRecord:
|
|
2496
|
+
type: object
|
|
2497
|
+
required: [id, retired, replacement, announcedAt, announcedBy]
|
|
2498
|
+
properties:
|
|
2499
|
+
id: { type: string }
|
|
2500
|
+
retired: { $ref: '#/components/schemas/RetiredBundleVersionRef' }
|
|
2501
|
+
replacement: { $ref: '#/components/schemas/RetiredBundleVersionRef' }
|
|
2502
|
+
announcedAt: { type: string }
|
|
2503
|
+
announcedBy: { type: string }
|
|
2504
|
+
|
|
2505
|
+
BundleVersionRetirement:
|
|
2506
|
+
description: 'An add-on announcement as the list answers it, with how far it has come'
|
|
2507
|
+
allOf:
|
|
2508
|
+
- $ref: '#/components/schemas/BundleVersionRetirementRecord'
|
|
2509
|
+
- type: object
|
|
2510
|
+
required: [progress]
|
|
2511
|
+
properties:
|
|
2512
|
+
progress:
|
|
2513
|
+
type: object
|
|
2514
|
+
description: 'Counted over the bookings it reached'
|
|
2515
|
+
required: [moved, waiting, overdue, ended, notTold, reminded]
|
|
2516
|
+
properties:
|
|
2517
|
+
moved: { type: integer }
|
|
2518
|
+
waiting: { type: integer }
|
|
2519
|
+
overdue: { type: integer }
|
|
2520
|
+
ended: { type: integer }
|
|
2521
|
+
notTold: { type: integer }
|
|
2522
|
+
reminded: { type: integer }
|
|
2523
|
+
|
|
2524
|
+
BundleRetirementAnnounced:
|
|
2525
|
+
type: object
|
|
2526
|
+
required: [retirement, told, failed]
|
|
2527
|
+
properties:
|
|
2528
|
+
retirement: { $ref: '#/components/schemas/BundleVersionRetirementRecord' }
|
|
2529
|
+
told: { type: integer, description: 'Notices sent at once' }
|
|
2530
|
+
failed: { type: integer, description: 'Notices the next run sends' }
|
|
2531
|
+
|
|
1928
2532
|
CatalogMarketingProjectionUpdate:
|
|
1929
2533
|
type: object
|
|
1930
2534
|
description: 'Partial update of a marketing projection'
|
|
@@ -2169,6 +2773,24 @@ components:
|
|
|
2169
2773
|
sendWelcomeMail: { type: boolean, default: true }
|
|
2170
2774
|
|
|
2171
2775
|
# ────────── Subscription ──────────
|
|
2776
|
+
CustomLimits:
|
|
2777
|
+
type: object
|
|
2778
|
+
description: >-
|
|
2779
|
+
A tenant's own limits beside its plan — a negotiated contract, a pilot
|
|
2780
|
+
(`CustomLimits` in @saasicat/core). A quota replaces the plan's value for that
|
|
2781
|
+
quota; a feature is added to the plan's. Stored as JSON in
|
|
2782
|
+
`subscriptions.customLimits`; a stored value in any other shape is not applied.
|
|
2783
|
+
additionalProperties: false
|
|
2784
|
+
properties:
|
|
2785
|
+
quotas:
|
|
2786
|
+
type: object
|
|
2787
|
+
additionalProperties:
|
|
2788
|
+
type: integer
|
|
2789
|
+
minimum: -1
|
|
2790
|
+
description: 'quotaKey → maxValue (-1 = unlimited)'
|
|
2791
|
+
features:
|
|
2792
|
+
type: array
|
|
2793
|
+
items: { type: string }
|
|
2172
2794
|
SubscriptionDetail:
|
|
2173
2795
|
type: object
|
|
2174
2796
|
properties:
|
|
@@ -2181,9 +2803,7 @@ components:
|
|
|
2181
2803
|
canceledAt: { type: string, format: date-time }
|
|
2182
2804
|
isPilot: { type: boolean }
|
|
2183
2805
|
pilotEndsAt: { type: string, format: date-time }
|
|
2184
|
-
customLimits:
|
|
2185
|
-
type: object
|
|
2186
|
-
additionalProperties: { type: integer }
|
|
2806
|
+
customLimits: { $ref: '#/components/schemas/CustomLimits' }
|
|
2187
2807
|
customMonthlyNet: { type: number, format: decimal, nullable: true }
|
|
2188
2808
|
effectiveLimits:
|
|
2189
2809
|
type: object
|
|
@@ -2227,6 +2847,139 @@ components:
|
|
|
2227
2847
|
contract was concluded; null where that line
|
|
2228
2848
|
cannot be read.
|
|
2229
2849
|
|
|
2850
|
+
SubscriberReadiness:
|
|
2851
|
+
type: object
|
|
2852
|
+
description: >-
|
|
2853
|
+
Whether a subscriber can be given its next contract where a tax
|
|
2854
|
+
adapter decides: the empty fields of the address an invoice
|
|
2855
|
+
names, and the adapter's sentence where it supports no
|
|
2856
|
+
treatment for the subscriber as it stands.
|
|
2857
|
+
required: [ready, missing, taxRefusal]
|
|
2858
|
+
properties:
|
|
2859
|
+
ready: { type: boolean }
|
|
2860
|
+
missing:
|
|
2861
|
+
type: array
|
|
2862
|
+
items:
|
|
2863
|
+
type: string
|
|
2864
|
+
enum: [addressLine1, postalCode, city, country]
|
|
2865
|
+
taxRefusal:
|
|
2866
|
+
type: string
|
|
2867
|
+
nullable: true
|
|
2868
|
+
|
|
2869
|
+
AdminTenantSubscriber:
|
|
2870
|
+
type: object
|
|
2871
|
+
required: [subscriber, readiness]
|
|
2872
|
+
properties:
|
|
2873
|
+
subscriber:
|
|
2874
|
+
type: object
|
|
2875
|
+
nullable: true
|
|
2876
|
+
description: Null for a tenant that has no subscriber.
|
|
2877
|
+
required:
|
|
2878
|
+
[
|
|
2879
|
+
id,
|
|
2880
|
+
customerNumber,
|
|
2881
|
+
legalName,
|
|
2882
|
+
addressLine1,
|
|
2883
|
+
addressLine2,
|
|
2884
|
+
postalCode,
|
|
2885
|
+
city,
|
|
2886
|
+
country,
|
|
2887
|
+
business,
|
|
2888
|
+
vatId,
|
|
2889
|
+
vatIdValidated,
|
|
2890
|
+
taxNumber,
|
|
2891
|
+
migrated,
|
|
2892
|
+
]
|
|
2893
|
+
properties:
|
|
2894
|
+
id: { type: string }
|
|
2895
|
+
customerNumber: { type: string }
|
|
2896
|
+
legalName: { type: string }
|
|
2897
|
+
addressLine1: { type: string, nullable: true }
|
|
2898
|
+
addressLine2: { type: string, nullable: true }
|
|
2899
|
+
postalCode: { type: string, nullable: true }
|
|
2900
|
+
city: { type: string, nullable: true }
|
|
2901
|
+
country: { type: string, nullable: true }
|
|
2902
|
+
business: { type: boolean, nullable: true }
|
|
2903
|
+
vatId: { type: string, nullable: true }
|
|
2904
|
+
vatIdValidated:
|
|
2905
|
+
type: boolean
|
|
2906
|
+
description: Whether the check that counts for the VAT id found it valid.
|
|
2907
|
+
taxNumber: { type: string, nullable: true }
|
|
2908
|
+
migrated:
|
|
2909
|
+
type: boolean
|
|
2910
|
+
description: Created from the application's own tenant record.
|
|
2911
|
+
readiness:
|
|
2912
|
+
nullable: true
|
|
2913
|
+
description: Null where no tax adapter decides, or the tenant has no subscriber.
|
|
2914
|
+
allOf:
|
|
2915
|
+
- $ref: '#/components/schemas/SubscriberReadiness'
|
|
2916
|
+
|
|
2917
|
+
AdminVatIdCheckOutcome:
|
|
2918
|
+
description: >-
|
|
2919
|
+
A check of a VAT number as the operator is shown it — what the
|
|
2920
|
+
service found, or why the check did not complete.
|
|
2921
|
+
oneOf:
|
|
2922
|
+
- type: object
|
|
2923
|
+
required: [completed, valid, checkedAt, service, counts]
|
|
2924
|
+
properties:
|
|
2925
|
+
completed: { type: boolean, enum: [true] }
|
|
2926
|
+
valid: { type: boolean }
|
|
2927
|
+
checkedAt: { type: string, format: date-time }
|
|
2928
|
+
service: { type: string }
|
|
2929
|
+
counts:
|
|
2930
|
+
type: boolean
|
|
2931
|
+
description: Whether it is the check that counts for the number held now.
|
|
2932
|
+
- type: object
|
|
2933
|
+
required: [completed, reason]
|
|
2934
|
+
properties:
|
|
2935
|
+
completed: { type: boolean, enum: [false] }
|
|
2936
|
+
reason: { type: string }
|
|
2937
|
+
|
|
2938
|
+
AdminSubscriberCorrected:
|
|
2939
|
+
type: object
|
|
2940
|
+
required: [subscriber, vatIdCheck]
|
|
2941
|
+
properties:
|
|
2942
|
+
subscriber: { $ref: '#/components/schemas/AdminTenantSubscriber' }
|
|
2943
|
+
vatIdCheck:
|
|
2944
|
+
nullable: true
|
|
2945
|
+
description: The check a VAT number got, where one was made.
|
|
2946
|
+
allOf:
|
|
2947
|
+
- $ref: '#/components/schemas/AdminVatIdCheckOutcome'
|
|
2948
|
+
|
|
2949
|
+
AdminSubscriberHistoryEntry:
|
|
2950
|
+
description: One correction, change of the tax origin, or check of the VAT number.
|
|
2951
|
+
oneOf:
|
|
2952
|
+
- type: object
|
|
2953
|
+
required: [kind, at, by, previous, corrected, reason]
|
|
2954
|
+
properties:
|
|
2955
|
+
kind: { type: string, enum: [identity-corrected] }
|
|
2956
|
+
at: { type: string, format: date-time }
|
|
2957
|
+
by: { type: string }
|
|
2958
|
+
previous: { type: object, additionalProperties: true }
|
|
2959
|
+
corrected: { type: object, additionalProperties: true }
|
|
2960
|
+
reason: { type: string }
|
|
2961
|
+
- type: object
|
|
2962
|
+
required: [kind, at, by, previous, changed, reason]
|
|
2963
|
+
properties:
|
|
2964
|
+
kind: { type: string, enum: [tax-origin-changed] }
|
|
2965
|
+
at: { type: string, format: date-time }
|
|
2966
|
+
by: { type: string }
|
|
2967
|
+
previous: { type: object, additionalProperties: true }
|
|
2968
|
+
changed: { type: object, additionalProperties: true }
|
|
2969
|
+
reason:
|
|
2970
|
+
type: string
|
|
2971
|
+
nullable: true
|
|
2972
|
+
description: Null for a change of the country with the contact details.
|
|
2973
|
+
- type: object
|
|
2974
|
+
required: [kind, at, vatId, valid, service, counts]
|
|
2975
|
+
properties:
|
|
2976
|
+
kind: { type: string, enum: [vat-id-checked] }
|
|
2977
|
+
at: { type: string, format: date-time }
|
|
2978
|
+
vatId: { type: string }
|
|
2979
|
+
valid: { type: boolean }
|
|
2980
|
+
service: { type: string }
|
|
2981
|
+
counts: { type: boolean }
|
|
2982
|
+
|
|
2230
2983
|
# ────────── User ──────────
|
|
2231
2984
|
AdminUserRow:
|
|
2232
2985
|
type: object
|