@saasicat/spec 1.0.0-rc.24 → 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.
@@ -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.25
33
33
  description: |
34
34
  Read and write operations for platform administration:
35
35
  tenants, users, subscriptions, promo codes, audit log,
@@ -387,6 +387,201 @@ paths:
387
387
  '404':
388
388
  description: No tenant with this slug (`TENANT_NOT_FOUND`)
389
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
+
390
585
  # ────────────────────────────────────────────────────────
391
586
  # Users
392
587
  # ────────────────────────────────────────────────────────
@@ -812,8 +1007,9 @@ paths:
812
1007
  id: { type: string }
813
1008
  tenant:
814
1009
  type: object
815
- required: [slug, name]
1010
+ required: [id, slug, name]
816
1011
  properties:
1012
+ id: { type: string }
817
1013
  slug: { type: string }
818
1014
  name: { type: string }
819
1015
  plan: { type: string }
@@ -1084,6 +1280,80 @@ paths:
1084
1280
  type: array
1085
1281
  items: { $ref: '#/components/schemas/VersionRetirement' }
1086
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
+
1087
1357
  # ── Bundles ──
1088
1358
  /catalog/bundles:
1089
1359
  get:
@@ -2114,6 +2384,151 @@ components:
2114
2384
  told: { type: integer, description: 'Notices sent at once' }
2115
2385
  failed: { type: integer, description: 'Notices the next run sends' }
2116
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
+
2117
2532
  CatalogMarketingProjectionUpdate:
2118
2533
  type: object
2119
2534
  description: 'Partial update of a marketing projection'
@@ -2432,6 +2847,139 @@ components:
2432
2847
  contract was concluded; null where that line
2433
2848
  cannot be read.
2434
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
+
2435
2983
  # ────────── User ──────────
2436
2984
  AdminUserRow:
2437
2985
  type: object
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saasicat/spec",
3
- "version": "1.0.0-rc.24",
3
+ "version": "1.0.0-rc.25",
4
4
  "description": "Language-neutral spec of the SaaS platform: JSON Schemas, OpenAPI contract, Prisma fragments, acceptance scenarios.",
5
5
  "type": "module",
6
6
  "main": "./index.cjs",
@@ -90,9 +90,10 @@ model BundleVersion {
90
90
  bundle Bundle @relation(fields: [bundleId], references: [id], onDelete: Cascade)
91
91
  baseVersion BundleVersion? @relation("BundleVersionLineage", fields: [baseVersionId], references: [id])
92
92
  derivedVersions BundleVersion[] @relation("BundleVersionLineage")
93
- // Standalone add-on bookings pinned to this version (see
94
- // 11-subscription-bundle.prisma).
95
- subscriptionBundles SubscriptionBundle[]
93
+ // Standalone add-on bookings pinned to this version, and the ones set to
94
+ // continue on it at the end of their term (see 11-subscription-bundle.prisma).
95
+ subscriptionBundles SubscriptionBundle[] @relation("SubscriptionBundleVersion")
96
+ subscriptionBundlesPending SubscriptionBundle[] @relation("SubscriptionBundlePendingVersion")
96
97
 
97
98
  @@unique([bundleId, version])
98
99
  @@index([bundleId, supersededAt])
@@ -47,6 +47,13 @@ model SubscriptionContract {
47
47
  // what was agreed.
48
48
  partiesMigrated Boolean @default(false)
49
49
 
50
+ // The tax treatment the installation's tax adapter decided when the
51
+ // contract was concluded: its kind, rate and note, with the adapter's name
52
+ // and version (ADR 0013). Null where no adapter was asked — an installation
53
+ // without one, a contract from before, or a path that does not ask one
54
+ // yet; each invoice then takes the adapter's answer when it is issued.
55
+ taxTreatment Json?
56
+
50
57
  status SubscriptionContractStatus @default(active)
51
58
  effectiveFrom DateTime
52
59
  effectiveUntil DateTime?
@@ -81,6 +81,12 @@ model PendingRegistration {
81
81
  country String? // ISO 3166-1 alpha-2
82
82
  vatId String?
83
83
  taxNumber String?
84
+ // Whether the sign-up is a business, which a tax adapter decides from; and
85
+ // the check of `vatId` made in step 4, outside any transaction, where the
86
+ // adapter needed a validated number. The subscriber takes the check over
87
+ // when it is created, so its first contract is decided from it.
88
+ business Boolean?
89
+ vatIdCheck Json?
84
90
 
85
91
  // The payment gateway's session for the payment method. A session id is
86
92
  // unique only within the gateway account that opened it, so the account
@@ -46,6 +46,14 @@ model SubscriptionBundle {
46
46
  currentPeriodStart DateTime?
47
47
  currentPeriodEnd DateTime?
48
48
 
49
+ // A switch to a newer version of the add-on that takes something away,
50
+ // taken for the end of the booking's running term: the version it continues
51
+ // on, and from when. Both null, or both set — `constraints.postgres.sql`
52
+ // holds them to that. The platform's quarter-hourly run makes the switch at
53
+ // that moment and clears both.
54
+ pendingBundleVersionId String?
55
+ pendingVersionEffectiveAt DateTime?
56
+
49
57
  // Cancellation: requested at `canceledAt`, effective at
50
58
  // `canceledEffectiveAt` (period/term end). Both null = active booking.
51
59
  canceledAt DateTime?
@@ -56,11 +64,13 @@ model SubscriptionBundle {
56
64
 
57
65
  // The subscription this add-on is booked on. Restrict the BundleVersion so
58
66
  // a booked version cannot be hard-deleted from the catalog.
59
- subscription Subscription @relation(fields: [subscriptionId], references: [id], onDelete: Cascade)
60
- bundleVersion BundleVersion @relation(fields: [bundleVersionId], references: [id], onDelete: Restrict)
67
+ subscription Subscription @relation(fields: [subscriptionId], references: [id], onDelete: Cascade)
68
+ bundleVersion BundleVersion @relation("SubscriptionBundleVersion", fields: [bundleVersionId], references: [id], onDelete: Restrict)
69
+ pendingBundleVersion BundleVersion? @relation("SubscriptionBundlePendingVersion", fields: [pendingBundleVersionId], references: [id], onDelete: Restrict)
61
70
 
62
71
  @@index([subscriptionId])
63
72
  @@index([bundleVersionId])
64
73
  @@index([canceledEffectiveAt])
74
+ @@index([pendingVersionEffectiveAt])
65
75
  @@map("subscription_bundles")
66
76
  }