@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
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.26",
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?
@@ -66,6 +73,7 @@ model SubscriptionContract {
66
73
 
67
74
  lineItems ContractLineItem[]
68
75
  ledgerEntries SubscriberLedgerEntry[] // see 15-subscriber-ledger.prisma
76
+ invoices SubscriptionInvoice[] // see 20-subscription-invoice.prisma
69
77
 
70
78
  createdAt DateTime @default(now())
71
79
  updatedAt DateTime @updatedAt
@@ -124,6 +132,7 @@ model ContractLineItem {
124
132
 
125
133
  contract SubscriptionContract @relation(fields: [contractId], references: [id], onDelete: Cascade)
126
134
  ledgerEntries SubscriberLedgerEntry[] // see 15-subscriber-ledger.prisma
135
+ invoiceLines SubscriptionInvoiceLine[] // see 20-subscription-invoice.prisma
127
136
 
128
137
  @@index([contractId, kind])
129
138
  @@index([sourceVersionId])
@@ -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
  }
@@ -1,5 +1,6 @@
1
1
  // =============================================================================
2
- // SaaS platform Prisma fragment: Subscriber, its tenants, its corrections
2
+ // SaaS platform Prisma fragment: Subscriber, its tenants, its corrections,
3
+ // the changes of its tax origin, the checks of its VAT id
3
4
  // =============================================================================
4
5
  //
5
6
  // REFERENCE SNIPPET — see 01-subscription.prisma for conventions.
@@ -32,6 +33,25 @@ model Subscriber {
32
33
  vatId String?
33
34
  taxNumber String?
34
35
 
36
+ // The tax origin beside the country and the VAT id (ADR 0013): whether the
37
+ // subscriber is a business, as sign-up or the operator recorded it — null
38
+ // while nobody has said, and never derived from a tax identifier, since a
39
+ // business outside the European Union may have none. A change of the
40
+ // country, the business status or the VAT id is recorded in
41
+ // `SubscriberTaxOriginChange`.
42
+ business Boolean?
43
+
44
+ // Which recorded check of the VAT id counts now (`SubscriberVatIdCheck`):
45
+ // the latest completed one of the number the subscriber has. Null while
46
+ // none does — never checked, or the number corrected since. A reference
47
+ // without a relation: the check already points at its subscriber, and a
48
+ // key in both directions would tie each row to the other.
49
+ currentVatIdCheckId String?
50
+ // Since when the VAT id is held, set by the correction that wrote it; null
51
+ // while it is the one the subscriber was created with. A check completed
52
+ // before it does not count for the number.
53
+ vatIdSince DateTime?
54
+
35
55
  // Contact details, which change at any time.
36
56
  addressLine1 String?
37
57
  addressLine2 String?
@@ -47,12 +67,15 @@ model Subscriber {
47
67
  createdAt DateTime @default(now())
48
68
  updatedAt DateTime @updatedAt
49
69
 
50
- tenants SubscriberTenant[]
51
- corrections SubscriberCorrection[]
52
- contracts SubscriptionContract[]
70
+ tenants SubscriberTenant[]
71
+ corrections SubscriberCorrection[]
72
+ taxOriginChanges SubscriberTaxOriginChange[]
73
+ vatIdChecks SubscriberVatIdCheck[]
74
+ contracts SubscriptionContract[]
53
75
  paymentMethods SubscriberPaymentMethod[] // see 14-payments.prisma
54
76
  paymentMethodSetups SubscriberPaymentMethodSetup[]
55
77
  ledgerEntries SubscriberLedgerEntry[] // see 15-subscriber-ledger.prisma
78
+ invoices SubscriptionInvoice[] // see 20-subscription-invoice.prisma
56
79
 
57
80
  @@map("subscribers")
58
81
  }
@@ -76,11 +99,14 @@ model SubscriberTenant {
76
99
  }
77
100
 
78
101
  // A correction of a subscriber's legal identity: the values it replaced, the
79
- // values it wrote, why, and who made it. Only the fields that moved are in the
80
- // two JSON objects. Written in the same transaction as the change, and never
81
- // rewritten.
102
+ // values it wrote, why, who made it, and when the write held the subscriber's
103
+ // lock. Only the fields that moved are in the two JSON objects. Written in the
104
+ // same transaction as the change, and never rewritten.
82
105
  model SubscriberCorrection {
83
106
  id String @id @default(uuid())
107
+ // The order the corrections were written in, numbered by the database at
108
+ // the write, and the order the list is read in. Never set by the platform.
109
+ seq Int @unique @default(autoincrement())
84
110
  subscriberId String
85
111
  previous Json
86
112
  corrected Json
@@ -90,6 +116,54 @@ model SubscriberCorrection {
90
116
 
91
117
  subscriber Subscriber @relation(fields: [subscriberId], references: [id], onDelete: Restrict)
92
118
 
93
- @@index([subscriberId, correctedAt])
119
+ @@index([subscriberId, seq])
94
120
  @@map("subscriber_corrections")
95
121
  }
122
+
123
+ // A change of a subscriber's tax origin — its country, whether it is a
124
+ // business, its VAT id — whichever way it arrived: a contact change, a
125
+ // correction of the legal identity, a change of the business status. The
126
+ // values it replaced, the values it wrote, who made it, and when the write
127
+ // held the subscriber's lock; only the fields that moved are in the two JSON
128
+ // objects. Written in the same transaction as the change, never rewritten, and
129
+ // applied from the subscriber's next invoice.
130
+ model SubscriberTaxOriginChange {
131
+ id String @id @default(uuid())
132
+ // The order the changes were written in, numbered by the database at the
133
+ // write, and the order the list is read in. Never set by the platform.
134
+ seq Int @unique @default(autoincrement())
135
+ subscriberId String
136
+ previous Json
137
+ changed Json
138
+ changedBy String
139
+ changedAt DateTime
140
+ // Why: the reason of the correction or of the change of business status
141
+ // that made it; null for a change of the country with the contact details.
142
+ reason String?
143
+
144
+ subscriber Subscriber @relation(fields: [subscriberId], references: [id], onDelete: Restrict)
145
+
146
+ @@index([subscriberId, seq])
147
+ @@map("subscriber_tax_origin_changes")
148
+ }
149
+
150
+ // A completed check of a subscriber's VAT id, as the service answered it: the
151
+ // number checked, when the answer came, whether it found the number valid, the
152
+ // service and its confirmation. Recorded for every check, whether it counts or
153
+ // not, and never rewritten: a reverse charge rests on the confirmation that
154
+ // held when the invoice was issued (ADR 0013).
155
+ model SubscriberVatIdCheck {
156
+ id String @id @default(uuid())
157
+ subscriberId String
158
+ vatId String
159
+ checkedAt DateTime
160
+ valid Boolean
161
+ service String
162
+ confirmation Json
163
+ recordedAt DateTime @default(now())
164
+
165
+ subscriber Subscriber @relation(fields: [subscriberId], references: [id], onDelete: Restrict)
166
+
167
+ @@index([subscriberId, checkedAt])
168
+ @@map("subscriber_vat_id_checks")
169
+ }
@@ -23,7 +23,7 @@
23
23
  //
24
24
  // `origin` and `source` are text holding a closed set, checked when a row is
25
25
  // read (`toSubscriberChargeRecord` in @saasicat/core):
26
- // origin: activation | renewal | planChange | bundleBooking | credit | correction
26
+ // origin: activation | renewal | planChange | bundleBooking | bundleChange | credit | correction
27
27
  // source: plan | bundle | discount
28
28
  //
29
29
  // Optional: adopt it where `tenantBilling.chargeJournal` is configured, which
@@ -57,6 +57,7 @@ model SubscriberLedgerEntry {
57
57
  subscriber Subscriber @relation(fields: [subscriberId], references: [id], onDelete: Restrict)
58
58
  contract SubscriptionContract @relation(fields: [contractId], references: [id], onDelete: Restrict)
59
59
  contractLineItem ContractLineItem @relation(fields: [contractLineItemId], references: [id], onDelete: Restrict)
60
+ invoiceLine SubscriptionInvoiceLine? // see 20-subscription-invoice.prisma
60
61
 
61
62
  @@unique([subscriptionId, source, sourceRef, periodStart, origin])
62
63
  @@index([subscriberId, bookedAt])
@@ -21,7 +21,8 @@
21
21
  // `subscriptionId` are kept as values, not as keys.
22
22
  //
23
23
  // `kind` is text holding a closed set: version-offered, version-retired,
24
- // version-retirement-reminder.
24
+ // version-retirement-reminder, bundle-version-retired,
25
+ // bundle-version-retirement-reminder.
25
26
  //
26
27
  // Optional: adopt it where `tenantBilling.versionNotices` is configured, which
27
28
  // refuses to start without a place to keep notices; a retirement needs it too.
@@ -37,7 +38,9 @@ model SubscriptionNotice {
37
38
  // What the notice is about — for version-offered, the id of the version
38
39
  // offered; for version-retired and version-retirement-reminder, the id of
39
40
  // the version retired, so a subscription is told of a version's retirement
40
- // once and reminded of it once.
41
+ // once and reminded of it once; for bundle-version-retired and
42
+ // bundle-version-retirement-reminder, the id of the add-on version retired,
43
+ // so the subscription's booking of it is told once and reminded of it once.
41
44
  subject String
42
45
  // The notice as it was last handed to the application.
43
46
  content Json
@@ -0,0 +1,44 @@
1
+ // =============================================================================
2
+ // SaaS platform Prisma fragment: BundleVersionRetirement
3
+ // =============================================================================
4
+ //
5
+ // REFERENCE SNIPPET — see 01-subscription.prisma for conventions.
6
+ //
7
+ // One row per add-on retirement an operator announces: an add-on version ended
8
+ // for the bookings already on it, with the version of the same add-on they
9
+ // continue on. Kept as evidence of what was announced, when and by whom, as
10
+ // `version_retirements` is for a plan version.
11
+ //
12
+ // What it means for each booking it reaches — the date it takes effect, the
13
+ // last day to cancel without the minimum term, and to whom and how that was
14
+ // told — is a `subscription_notices` row of kind bundle-version-retired,
15
+ // written in the same transaction: its subject is the retired add-on version,
16
+ // its content names this row's id as `retirementId`.
17
+ //
18
+ // The versions are kept as values, not as keys: the record outlives the
19
+ // catalogue entries it names, as the notices do (ADR 0012).
20
+ //
21
+ // Optional: adopt it where an operator may retire add-on versions
22
+ // (`tenantBilling.orderlyRetirement` in config/saas.yaml). Without it, retiring
23
+ // an add-on version is off; retiring a plan version is not affected.
24
+ //
25
+ // Contract: @saasicat/core (BundleVersionRetirementRepository).
26
+
27
+ model BundleVersionRetirement {
28
+ id String @id @default(uuid())
29
+
30
+ retiredBundleVersionId String
31
+ retiredBundleKey String
32
+ retiredVersion Int
33
+
34
+ replacementBundleVersionId String
35
+ replacementBundleKey String
36
+ replacementVersion Int
37
+
38
+ announcedAt DateTime @default(now())
39
+ // Who announced it — an actor tag, as the audit log writes it.
40
+ announcedBy String
41
+
42
+ @@index([retiredBundleVersionId])
43
+ @@map("bundle_version_retirements")
44
+ }
@@ -0,0 +1,145 @@
1
+ // =============================================================================
2
+ // SaaS platform Prisma fragment: SubscriptionInvoice
3
+ // =============================================================================
4
+ //
5
+ // REFERENCE SNIPPET — see 01-subscription.prisma for conventions.
6
+ //
7
+ // The invoices issued from the charge journal (15-subscriber-ledger.prisma):
8
+ // the charges a billing period opens with on one invoice, a charge that arises
9
+ // later on an invoice of its own. An invoice copies everything it states when
10
+ // it is issued — the issuer, the subscriber, the tax treatment the adapter
11
+ // decided, the tax per rate — so a later change leaves it as it was. Nothing
12
+ // updates or deletes an invoice; a correction is a cancellation invoice and its
13
+ // replacement.
14
+ //
15
+ // A line invoices one charge, and a charge stands on one line: the unique
16
+ // `chargeId` is what makes a second run issuing the same charges write
17
+ // nothing, however many run at the same time.
18
+ //
19
+ // The number range is the table `subscription_invoice_numbers`, one row per
20
+ // year holding the last number drawn. An invoice draws its number by raising
21
+ // that row in the transaction that writes the invoice, so an invoice that
22
+ // fails to be written leaves no gap, and two at the same time wait for each
23
+ // other rather than drawing the same number. One installation is the range of
24
+ // one issuer; the number is `<prefix>-<year>-<sequence>`.
25
+ //
26
+ // The days an invoice states — its issue date, its due date, the period its
27
+ // charges cover — are calendar days in the installation's time zone, kept as
28
+ // `date` columns; `issuedAt` is the instant it was issued.
29
+ //
30
+ // `origin` and `source` on a line are text holding the charge's closed sets,
31
+ // checked when a row is read (`toSubscriptionInvoiceRecord` in @saasicat/core).
32
+ //
33
+ // Optional: adopt it, all four models together, where
34
+ // `tenantBilling.chargeJournal.invoices` is configured, which needs the charge
35
+ // journal.
36
+ //
37
+ // Contract: @saasicat/core (SubscriptionInvoiceRepository).
38
+
39
+ model SubscriptionInvoice {
40
+ id String @id @default(uuid())
41
+ number String @unique
42
+ numberPrefix String
43
+ numberYear Int
44
+ numberSequence Int
45
+
46
+ tenantId String
47
+ subscriberId String
48
+ subscriptionId String
49
+ contractId String
50
+
51
+ issuedAt DateTime
52
+ issueDate DateTime @db.Date
53
+ dueDate DateTime @db.Date
54
+ servicePeriodFrom DateTime @db.Date
55
+ servicePeriodUntil DateTime @db.Date
56
+
57
+ currency String
58
+ // The parties as the invoice names them, copied when it was issued.
59
+ issuerParty Json
60
+ subscriberParty Json
61
+ // The footer lines config/saas.yaml named on the issue date (SC-PRIC-077).
62
+ footer Json @default("[]")
63
+ // The adapter's treatment, with its name and version, and the tax per rate.
64
+ taxTreatment Json
65
+ taxes Json
66
+ totalNet Decimal @db.Decimal(12, 2)
67
+ totalTax Decimal @db.Decimal(12, 2)
68
+ totalGross Decimal @db.Decimal(12, 2)
69
+
70
+ createdAt DateTime @default(now())
71
+
72
+ subscriber Subscriber @relation(fields: [subscriberId], references: [id], onDelete: Restrict)
73
+ contract SubscriptionContract @relation(fields: [contractId], references: [id], onDelete: Restrict)
74
+ lines SubscriptionInvoiceLine[]
75
+ document SubscriptionInvoiceDocument?
76
+ archive SubscriptionInvoiceArchive?
77
+
78
+ @@unique([numberYear, numberSequence])
79
+ @@index([subscriberId, issuedAt])
80
+ @@index([subscriptionId])
81
+ @@map("subscription_invoices")
82
+ }
83
+
84
+ model SubscriptionInvoiceLine {
85
+ id String @id @default(uuid())
86
+ invoiceId String
87
+ position Int
88
+ chargeId String @unique
89
+ contractLineItemId String
90
+
91
+ title String
92
+ origin String
93
+ source String
94
+
95
+ periodFrom DateTime @db.Date
96
+ periodUntil DateTime @db.Date
97
+
98
+ amountNet Decimal @db.Decimal(10, 2)
99
+ taxRate Decimal @db.Decimal(5, 2)
100
+
101
+ invoice SubscriptionInvoice @relation(fields: [invoiceId], references: [id], onDelete: Restrict)
102
+ charge SubscriberLedgerEntry @relation(fields: [chargeId], references: [id], onDelete: Restrict)
103
+ contractLineItem ContractLineItem @relation(fields: [contractLineItemId], references: [id], onDelete: Restrict)
104
+
105
+ @@unique([invoiceId, position])
106
+ @@index([contractLineItemId])
107
+ @@map("subscription_invoice_lines")
108
+ }
109
+
110
+ // Where an invoice's document stands. The checksum is recorded once, before
111
+ // the file is archived, and every later download is checked against it; the
112
+ // archive's confirmation and the delivery are set once each. A rendering with
113
+ // another checksum is refused rather than recorded over the first. A run claims
114
+ // the mail before it sends it, so of several instances one does.
115
+ model SubscriptionInvoiceDocument {
116
+ invoiceId String @id
117
+ // SHA-256 of the rendered file, 64 lower-case hex digits.
118
+ checksum String
119
+ mediaType String
120
+ fileName String
121
+ // In bytes.
122
+ size Int
123
+ renderedAt DateTime
124
+ // Null while the invoice waits for its document.
125
+ archivedAt DateTime?
126
+ // When the run mailing it took it on; null while no run holds it.
127
+ deliveryClaimedAt DateTime?
128
+ // Null until the invoice is handed to the mail.
129
+ deliveredAt DateTime?
130
+ deliveredTo String?
131
+
132
+ invoice SubscriptionInvoice @relation(fields: [invoiceId], references: [id], onDelete: Restrict)
133
+
134
+ @@index([archivedAt])
135
+ @@map("subscription_invoice_documents")
136
+ }
137
+
138
+ model SubscriptionInvoiceNumber {
139
+ // The year the numbers count in; the range restarts at 1 each year.
140
+ year Int @id
141
+ // The last number drawn in that year.
142
+ last Int
143
+
144
+ @@map("subscription_invoice_numbers")
145
+ }
@@ -0,0 +1,56 @@
1
+ // =============================================================================
2
+ // SaaS platform Prisma fragment: FeatureWithdrawal
3
+ // =============================================================================
4
+ //
5
+ // REFERENCE SNIPPET — see 01-subscription.prisma for conventions.
6
+ //
7
+ // One row per feature withdrawn for a reason outside the platform: an external
8
+ // service the feature depends on stopped, or a law took it away or changed it.
9
+ // From `effectiveFrom` the platform grants the feature to nobody, until the
10
+ // operator lifts the withdrawal from `liftedFrom`. Kept as evidence of what was
11
+ // announced, why, when and by whom, and of the reductions the operator named.
12
+ //
13
+ // What it means for each subscription it reaches — which lines, what each is
14
+ // reduced by — is a `subscription_notices` row of kind feature-withdrawn whose
15
+ // subject is this row's id. Those are written once this row is: who it reaches
16
+ // is read after it is recorded, so a change concluded while it was being
17
+ // announced is reached, and `toldAt` is set in the transaction that writes
18
+ // them. A row whose `toldAt` is still null is told by the next run.
19
+ //
20
+ // `reductions` is a JSON list of `{ kind, key, billingCycle, amountNet }`: the
21
+ // net amount a plan or an add-on, by its key, is reduced by for one whole
22
+ // period of that rhythm without the feature.
23
+ //
24
+ // At most ONE withdrawal of a feature is not lifted at a time. Prisma cannot
25
+ // express a partial unique index; the one that holds it is in
26
+ // sql/constraints.postgres.sql, and it is what makes two operators announcing
27
+ // at the same moment land one withdrawal rather than two.
28
+ //
29
+ // Optional: adopt it where an operator may withdraw features. Without it,
30
+ // withdrawing a feature is off.
31
+ //
32
+ // Contract: @saasicat/core (FeatureWithdrawalRepository).
33
+
34
+ model FeatureWithdrawal {
35
+ id String @id @default(uuid())
36
+ featureKey String
37
+ // Why, in the operator's words, shown to the subscribers as written.
38
+ reason String
39
+
40
+ effectiveFrom DateTime
41
+ // Null until the operator lifts it.
42
+ liftedFrom DateTime?
43
+
44
+ reductions Json @default("[]")
45
+
46
+ announcedAt DateTime @default(now())
47
+ // Who announced it, and who lifted it — actor tags, as the audit log writes them.
48
+ announcedBy String
49
+ liftedAt DateTime?
50
+ liftedBy String?
51
+ // When the subscriptions it reaches were told; null until then.
52
+ toldAt DateTime?
53
+
54
+ @@index([featureKey])
55
+ @@map("feature_withdrawals")
56
+ }
@@ -0,0 +1,34 @@
1
+ // =============================================================================
2
+ // SaaSiCat platform Prisma fragment: SubscriptionInvoiceArchive
3
+ // =============================================================================
4
+ //
5
+ // REFERENCE SNIPPET — see 01-subscription.prisma for conventions.
6
+ //
7
+ // The database archive of the invoices' documents (20-subscription-invoice.prisma):
8
+ // one file per invoice, under its invoice, with the checksum recorded when it
9
+ // was rendered. A row is written once and never changed: the migration
10
+ // `1.0-an-invoice-document-is-archived.postgres.sql` installs a trigger that
11
+ // refuses every UPDATE and DELETE, whoever asks. Keep it, with its database
12
+ // backups, as long as the law asks — in Germany 8 years from the end of the
13
+ // calendar year an invoice was issued in — and in the European Union.
14
+ //
15
+ // Optional: adopt it where the invoices' documents are kept in the database,
16
+ // as `persistence.entitlement.subscriptionInvoiceArchive` keeps them. An
17
+ // installation that archives elsewhere, such as object storage with a
18
+ // retention lock, binds its own `InvoiceArchivePort` instead.
19
+ //
20
+ // Contract: @saasicat/core (InvoiceArchivePort).
21
+
22
+ model SubscriptionInvoiceArchive {
23
+ invoiceId String @id
24
+ // SHA-256 of `content`, 64 lower-case hex digits.
25
+ checksum String
26
+ mediaType String
27
+ fileName String
28
+ content Bytes
29
+ archivedAt DateTime
30
+
31
+ invoice SubscriptionInvoice @relation(fields: [invoiceId], references: [id], onDelete: Restrict)
32
+
33
+ @@map("subscription_invoice_archive")
34
+ }
@@ -28,25 +28,30 @@ regenerated after fragment changes (`tests/reference-sql-drift.test.js`).
28
28
 
29
29
  ## Files
30
30
 
31
- | File | Models |
32
- | -------------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
33
- | [`01-subscription.prisma`](01-subscription.prisma) | `Subscription`, `CheckoutOffer` + Enums |
34
- | [`02-promo-code.prisma`](02-promo-code.prisma) | `PromoCode`, `PromoCodeRedemption`, `PromoCodeHold`, `PromoCodeValidationLog` + Enums |
35
- | [`03-plan-versions.prisma`](03-plan-versions.prisma) | `Plan`, `PlanVersion` |
36
- | [`04-audit-log.prisma`](04-audit-log.prisma) | `AuditLog` |
37
- | [`05-bundle.prisma`](05-bundle.prisma) | `Bundle`, `BundleVersion` |
38
- | [`06-catalog-entries.prisma`](06-catalog-entries.prisma) | Capability, feature, quota and marketing catalog models |
39
- | [`07-promotion.prisma`](07-promotion.prisma) | `Promotion` |
40
- | [`08-subscription-contract.prisma`](08-subscription-contract.prisma) | `SubscriptionContract`, `ContractLineItem` |
41
- | [`09-pending-registration.prisma`](09-pending-registration.prisma) | `PendingRegistration` + `RegistrationStatus` |
42
- | [`10-super-admin.prisma`](10-super-admin.prisma) | `SuperAdminUser`, `SuperAdminMfa` |
43
- | [`11-subscription-bundle.prisma`](11-subscription-bundle.prisma) | `SubscriptionBundle` |
44
- | [`12-applied-settings.prisma`](12-applied-settings.prisma) | `AppliedSettings`, `SettingsChange` |
45
- | [`13-subscriber.prisma`](13-subscriber.prisma) | `Subscriber`, `SubscriberTenant`, `SubscriberCorrection` |
46
- | [`14-payments.prisma`](14-payments.prisma) | `SubscriberPaymentMethod`, `SubscriberPaymentMethodSetup`, `PaymentEventLog` |
47
- | [`15-subscriber-ledger.prisma`](15-subscriber-ledger.prisma) | `SubscriberLedgerEntry` |
48
- | [`16-maintenance-window.prisma`](16-maintenance-window.prisma) | `MaintenanceWindow` |
49
- | [`17-subscription-notice.prisma`](17-subscription-notice.prisma) | `SubscriptionNotice` |
31
+ | File | Models |
32
+ | ---------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
33
+ | [`01-subscription.prisma`](01-subscription.prisma) | `Subscription`, `CheckoutOffer` + Enums |
34
+ | [`02-promo-code.prisma`](02-promo-code.prisma) | `PromoCode`, `PromoCodeRedemption`, `PromoCodeHold`, `PromoCodeValidationLog` + Enums |
35
+ | [`03-plan-versions.prisma`](03-plan-versions.prisma) | `Plan`, `PlanVersion` |
36
+ | [`04-audit-log.prisma`](04-audit-log.prisma) | `AuditLog` |
37
+ | [`05-bundle.prisma`](05-bundle.prisma) | `Bundle`, `BundleVersion` |
38
+ | [`06-catalog-entries.prisma`](06-catalog-entries.prisma) | Capability, feature, quota and marketing catalog models |
39
+ | [`07-promotion.prisma`](07-promotion.prisma) | `Promotion` |
40
+ | [`08-subscription-contract.prisma`](08-subscription-contract.prisma) | `SubscriptionContract`, `ContractLineItem` |
41
+ | [`09-pending-registration.prisma`](09-pending-registration.prisma) | `PendingRegistration` + `RegistrationStatus` |
42
+ | [`10-super-admin.prisma`](10-super-admin.prisma) | `SuperAdminUser`, `SuperAdminMfa` |
43
+ | [`11-subscription-bundle.prisma`](11-subscription-bundle.prisma) | `SubscriptionBundle` |
44
+ | [`12-applied-settings.prisma`](12-applied-settings.prisma) | `AppliedSettings`, `SettingsChange` |
45
+ | [`13-subscriber.prisma`](13-subscriber.prisma) | `Subscriber`, `SubscriberTenant`, `SubscriberCorrection`, `SubscriberTaxOriginChange`, `SubscriberVatIdCheck` |
46
+ | [`14-payments.prisma`](14-payments.prisma) | `SubscriberPaymentMethod`, `SubscriberPaymentMethodSetup`, `PaymentEventLog` |
47
+ | [`15-subscriber-ledger.prisma`](15-subscriber-ledger.prisma) | `SubscriberLedgerEntry` |
48
+ | [`16-maintenance-window.prisma`](16-maintenance-window.prisma) | `MaintenanceWindow` |
49
+ | [`17-subscription-notice.prisma`](17-subscription-notice.prisma) | `SubscriptionNotice` |
50
+ | [`18-version-retirement.prisma`](18-version-retirement.prisma) | `VersionRetirement` |
51
+ | [`19-bundle-version-retirement.prisma`](19-bundle-version-retirement.prisma) | `BundleVersionRetirement` |
52
+ | [`20-subscription-invoice.prisma`](20-subscription-invoice.prisma) | `SubscriptionInvoice`, `SubscriptionInvoiceLine`, `SubscriptionInvoiceNumber`, `SubscriptionInvoiceDocument` |
53
+ | [`21-feature-withdrawal.prisma`](21-feature-withdrawal.prisma) | `FeatureWithdrawal` |
54
+ | [`22-subscription-invoice-archive.prisma`](22-subscription-invoice-archive.prisma) | `SubscriptionInvoiceArchive` |
50
55
 
51
56
  ## How the consumer uses the fragments
52
57
 
@@ -102,7 +107,8 @@ from ever being deleted.
102
107
  `quota_catalog_entries`, `marketing_projections`, `marketing_settings`,
103
108
  `promotions`, `subscription_contracts`, `contract_line_items`,
104
109
  `super_admin_users`, `super_admin_mfa`, `subscription_bundles`, `subscribers`,
105
- `subscriber_tenants`, `subscriber_corrections`, `subscriber_payment_methods`,
110
+ `subscriber_tenants`, `subscriber_corrections`, `subscriber_tax_origin_changes`,
111
+ `subscriber_vat_id_checks`, `subscriber_payment_methods`,
106
112
  `subscriber_payment_method_setups`.
107
113
  `PendingRegistration` and `PaymentEventLog` carry no `@@map` and keep Prisma's
108
114
  default names, `"PendingRegistration"` and `"PaymentEventLog"`.
@@ -142,7 +142,8 @@
142
142
  "platformEmail": { "$ref": "#/$defs/StandardPageDef" },
143
143
  "platformEmailHistory": { "$ref": "#/$defs/StandardPageDef" },
144
144
  "settings": { "$ref": "#/$defs/StandardPageDef" },
145
- "maintenance": { "$ref": "#/$defs/StandardPageDef" }
145
+ "maintenance": { "$ref": "#/$defs/StandardPageDef" },
146
+ "featureWithdrawals": { "$ref": "#/$defs/StandardPageDef" }
146
147
  }
147
148
  },
148
149
  "projectPages": {