@saasicat/spec 1.0.0-rc.2 → 1.0.0-rc.21
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/README.md +30 -0
- package/acceptance/README.md +3 -3
- package/acceptance/mfa/totp-verify-good-and-bad-code.yaml +6 -6
- package/admin-api.openapi.yaml +331 -77
- package/cli-conventions.md +19 -19
- package/index.cjs +3 -0
- package/index.d.cts +3 -1
- package/index.d.ts +2 -0
- package/index.js +9 -1
- package/package.json +4 -2
- package/prisma-fragments/01-subscription.prisma +34 -47
- package/prisma-fragments/02-promo-code.prisma +38 -0
- package/prisma-fragments/03-plan-versions.prisma +3 -4
- package/prisma-fragments/05-bundle.prisma +4 -5
- package/prisma-fragments/06-catalog-entries.prisma +24 -24
- package/prisma-fragments/07-promotion.prisma +2 -3
- package/prisma-fragments/08-subscription-contract.prisma +42 -5
- package/prisma-fragments/09-pending-registration.prisma +32 -27
- package/prisma-fragments/11-subscription-bundle.prisma +17 -0
- package/prisma-fragments/12-applied-settings.prisma +55 -0
- package/prisma-fragments/13-subscriber.prisma +94 -0
- package/prisma-fragments/14-payments.prisma +108 -0
- package/prisma-fragments/README.md +40 -22
- package/schemas/admin-manifest.schema.json +10 -3
- package/schemas/plan-catalog.schema.json +239 -18
- package/schemas/promo-code.schema.json +7 -1
- package/schemas/tenant-ledger.schema.json +236 -0
- package/sql/1.0-a-contract-names-its-subscriber.postgres.sql +383 -0
- package/sql/1.0-a-payment-method-is-a-gateway-reference.postgres.sql +177 -0
- package/sql/1.0-a-promo-slot-is-held-through-checkout.postgres.sql +69 -0
- package/sql/1.0-a-settings-change-carries-its-order.postgres.sql +73 -0
- package/sql/1.0-line-items-record-their-money.postgres.sql +134 -0
- package/sql/1.0-remove-project-key.postgres.sql +203 -0
- package/sql/1.0-the-applied-settings-are-recorded.postgres.sql +67 -0
- package/sql/constraints.postgres.sql +76 -0
- package/sql/reference-schema.postgres.sql +324 -77
|
@@ -29,6 +29,23 @@ model SubscriptionBundle {
|
|
|
29
29
|
// only at `minimumTermEndsAt`.
|
|
30
30
|
minimumTermEndsAt DateTime?
|
|
31
31
|
|
|
32
|
+
// The rhythm this booking is billed in, and the window it is billed for.
|
|
33
|
+
//
|
|
34
|
+
// A bundle may run in a shorter rhythm than its plan but never a longer
|
|
35
|
+
// one, so a yearly bundle exists only beside a yearly plan. Its periods end
|
|
36
|
+
// on the day the plan's do: the first one is short, from the booking to the
|
|
37
|
+
// next occurrence of that day, and every one after it runs anchor to
|
|
38
|
+
// anchor. Aligned at booking rather than trimmed at the end, because a trim
|
|
39
|
+
// means somebody was committed to more than they received.
|
|
40
|
+
//
|
|
41
|
+
// Null on a booking made before these columns existed, and on one whose
|
|
42
|
+
// plan has no period at all — a trial, or a subscription awaiting sales.
|
|
43
|
+
// Readers fall back to the plan's cycle, which is what every booking used
|
|
44
|
+
// before this.
|
|
45
|
+
billingCycle String?
|
|
46
|
+
currentPeriodStart DateTime?
|
|
47
|
+
currentPeriodEnd DateTime?
|
|
48
|
+
|
|
32
49
|
// Cancellation: requested at `canceledAt`, effective at
|
|
33
50
|
// `canceledEffectiveAt` (period/term end). Both null = active booking.
|
|
34
51
|
canceledAt DateTime?
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// SaaS-Platform Prisma fragment: the applied settings, and their changes
|
|
3
|
+
// =============================================================================
|
|
4
|
+
//
|
|
5
|
+
// REFERENCE SNIPPET — see 01-subscription.prisma for conventions.
|
|
6
|
+
//
|
|
7
|
+
// `config/saas.yaml` says what should be true. These two tables say what IS
|
|
8
|
+
// true: the settings the installation applied at its last start, and every
|
|
9
|
+
// start that found them changed. The platform writes both at boot and reads
|
|
10
|
+
// them for the read-only settings screen — and for nothing else. The record is
|
|
11
|
+
// a mirror of the file, never a source: no setting is ever read out of it.
|
|
12
|
+
//
|
|
13
|
+
// One row per installation. An installation serves one application and owns
|
|
14
|
+
// its database (see docs/explanation/data-model.md), so the row's identity is
|
|
15
|
+
// the installation's, and a constant primary key is what expresses that. The
|
|
16
|
+
// `CHECK` in sql/constraints.postgres.sql is what holds it to one row; the
|
|
17
|
+
// default only lands a caller that omits the id on the right one.
|
|
18
|
+
|
|
19
|
+
model AppliedSettings {
|
|
20
|
+
id String @id @default("installation")
|
|
21
|
+
// `sha256-<hex>` over the canonical JSON of `settings`.
|
|
22
|
+
fingerprint String
|
|
23
|
+
// The resolved settings subtree of the catalogue — everything but the
|
|
24
|
+
// plans and features, with environment references already substituted.
|
|
25
|
+
settings Json
|
|
26
|
+
// The absolute path of the file, or the phrase saying they came in as code.
|
|
27
|
+
source String
|
|
28
|
+
// When these values became the running configuration. Left alone by a
|
|
29
|
+
// start that finds them unchanged.
|
|
30
|
+
appliedAt DateTime
|
|
31
|
+
|
|
32
|
+
@@map("applied_settings")
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
model SettingsChange {
|
|
36
|
+
id String @id @default(uuid())
|
|
37
|
+
// The order the changes were recorded in, numbered by the database at the
|
|
38
|
+
// write that recorded each one, and the order the list is read in. Not
|
|
39
|
+
// `noticedAt`: that is the recording start's own clock, and a start delayed
|
|
40
|
+
// between reading it and writing would be listed before a move that landed
|
|
41
|
+
// earlier. Never set by the platform.
|
|
42
|
+
seq Int @unique @default(autoincrement())
|
|
43
|
+
// The start that found the fingerprint moved.
|
|
44
|
+
noticedAt DateTime
|
|
45
|
+
source String
|
|
46
|
+
previous Json
|
|
47
|
+
current Json
|
|
48
|
+
// Null while an operator still owes it a look.
|
|
49
|
+
acknowledgedAt DateTime?
|
|
50
|
+
// An actor tag, as the audit log writes it.
|
|
51
|
+
acknowledgedBy String?
|
|
52
|
+
|
|
53
|
+
@@index([acknowledgedAt, noticedAt])
|
|
54
|
+
@@map("settings_changes")
|
|
55
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// SaaS platform Prisma fragment: Subscriber, its tenants, its corrections
|
|
3
|
+
// =============================================================================
|
|
4
|
+
//
|
|
5
|
+
// REFERENCE SNIPPET — see 01-subscription.prisma for conventions.
|
|
6
|
+
//
|
|
7
|
+
// A tenant is where the application keeps a customer's data. The subscriber is
|
|
8
|
+
// the party the contract is with: a customer number and the master data a
|
|
9
|
+
// contract names. A tenant can be deleted; its subscriber stays for as long as
|
|
10
|
+
// a document that has to be kept belongs to it (ADR 0012). That is why nothing
|
|
11
|
+
// here points at the application's `Tenant` model with a cascade — the link
|
|
12
|
+
// below names the tenant as a value and stays after the tenant row is gone.
|
|
13
|
+
//
|
|
14
|
+
// Every model carries `Subscriber` in its name, so it cannot collide with a
|
|
15
|
+
// `Customer` an application already keeps for the people it sells to.
|
|
16
|
+
|
|
17
|
+
model Subscriber {
|
|
18
|
+
id String @id @default(uuid())
|
|
19
|
+
|
|
20
|
+
// The customer number, counted per installation. The number orders; the
|
|
21
|
+
// prefix is the one `config/saas.yaml` named when the subscriber was
|
|
22
|
+
// created, kept beside it so a later prefix renames nobody. Counting starts
|
|
23
|
+
// at 10001 (sql/constraints.postgres.sql), so numbers have five digits long
|
|
24
|
+
// before they need a sixth.
|
|
25
|
+
customerSequence Int @unique @default(autoincrement())
|
|
26
|
+
customerNumberPrefix String @default("")
|
|
27
|
+
|
|
28
|
+
// The legal identity. Only the name is required until invoicing requires
|
|
29
|
+
// the rest; under a running contract these change only as a correction,
|
|
30
|
+
// recorded in `SubscriberCorrection`.
|
|
31
|
+
legalName String
|
|
32
|
+
vatId String?
|
|
33
|
+
taxNumber String?
|
|
34
|
+
|
|
35
|
+
// Contact details, which change at any time.
|
|
36
|
+
addressLine1 String?
|
|
37
|
+
addressLine2 String?
|
|
38
|
+
postalCode String?
|
|
39
|
+
city String?
|
|
40
|
+
country String? // ISO 3166-1 alpha-2
|
|
41
|
+
invoiceEmail String?
|
|
42
|
+
|
|
43
|
+
// Created by the migration that gave every existing tenant its subscriber,
|
|
44
|
+
// from the application's own tenant record.
|
|
45
|
+
migrated Boolean @default(false)
|
|
46
|
+
|
|
47
|
+
createdAt DateTime @default(now())
|
|
48
|
+
updatedAt DateTime @updatedAt
|
|
49
|
+
|
|
50
|
+
tenants SubscriberTenant[]
|
|
51
|
+
corrections SubscriberCorrection[]
|
|
52
|
+
contracts SubscriptionContract[]
|
|
53
|
+
paymentMethods SubscriberPaymentMethod[] // see 14-payments.prisma
|
|
54
|
+
paymentMethodSetups SubscriberPaymentMethodSetup[]
|
|
55
|
+
|
|
56
|
+
@@map("subscribers")
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Which tenant a subscriber is live for, and which it was before. A subscriber
|
|
60
|
+
// has at most one live tenant and a tenant at most one live subscriber — two
|
|
61
|
+
// partial unique indexes in sql/constraints.postgres.sql, which Prisma cannot
|
|
62
|
+
// express. `unlinkedAt` stays null while the link is live.
|
|
63
|
+
model SubscriberTenant {
|
|
64
|
+
id String @id @default(uuid())
|
|
65
|
+
subscriberId String
|
|
66
|
+
tenantId String
|
|
67
|
+
linkedAt DateTime @default(now())
|
|
68
|
+
unlinkedAt DateTime?
|
|
69
|
+
|
|
70
|
+
subscriber Subscriber @relation(fields: [subscriberId], references: [id], onDelete: Restrict)
|
|
71
|
+
|
|
72
|
+
@@index([tenantId])
|
|
73
|
+
@@index([subscriberId])
|
|
74
|
+
@@map("subscriber_tenants")
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// A correction of a subscriber's legal identity: the values it replaced, the
|
|
78
|
+
// values it wrote, why, and who made it. Only the fields that moved are in the
|
|
79
|
+
// two JSON objects. Written in the same transaction as the change, and never
|
|
80
|
+
// rewritten.
|
|
81
|
+
model SubscriberCorrection {
|
|
82
|
+
id String @id @default(uuid())
|
|
83
|
+
subscriberId String
|
|
84
|
+
previous Json
|
|
85
|
+
corrected Json
|
|
86
|
+
reason String
|
|
87
|
+
correctedBy String
|
|
88
|
+
correctedAt DateTime
|
|
89
|
+
|
|
90
|
+
subscriber Subscriber @relation(fields: [subscriberId], references: [id], onDelete: Restrict)
|
|
91
|
+
|
|
92
|
+
@@index([subscriberId, correctedAt])
|
|
93
|
+
@@map("subscriber_corrections")
|
|
94
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// SaaS platform Prisma fragment: SubscriberPaymentMethod, its setups, PaymentEventLog
|
|
3
|
+
// =============================================================================
|
|
4
|
+
//
|
|
5
|
+
// REFERENCE SNIPPET — see 01-subscription.prisma for conventions.
|
|
6
|
+
//
|
|
7
|
+
// How a subscriber pays, as the payment gateway's reference to it. The card
|
|
8
|
+
// number or the IBAN never reaches this table: the person enters them in the
|
|
9
|
+
// gateway's own form, and what is kept is the gateway's reference and the
|
|
10
|
+
// masked details that tell one payment method from another (ADR 0012).
|
|
11
|
+
//
|
|
12
|
+
// A reference means something only to the gateway account that issued it —
|
|
13
|
+
// the merchant account named in `config/saas.yaml#payments.accounts` — so
|
|
14
|
+
// every row records that account. A subscriber has at most one `ACTIVE`
|
|
15
|
+
// payment method, a partial unique index in sql/constraints.postgres.sql;
|
|
16
|
+
// the one a newer payment method replaced stays as `REPLACED`.
|
|
17
|
+
//
|
|
18
|
+
// Within an account a reference belongs to exactly one subscriber, which is
|
|
19
|
+
// what `@@unique([gatewayAccount, paymentMethodRef])` below holds. The port
|
|
20
|
+
// draws the same boundary for reading: a confirmation carrying a reference
|
|
21
|
+
// another subscriber holds is refused rather than recorded, and a read by
|
|
22
|
+
// reference names the subscriber it asks about. Both are reached on the
|
|
23
|
+
// gateway's callback, which arrives without a session — an installation that
|
|
24
|
+
// keeps its tenants apart with a policy on this table lifts it there.
|
|
25
|
+
//
|
|
26
|
+
// Contract: @saasicat/core (SubscriberPaymentMethodRepository, PaymentEventLog).
|
|
27
|
+
// Both are needed wherever payment methods are taken — self-registration or
|
|
28
|
+
// not — because every gateway callback is claimed in `PaymentEventLog`.
|
|
29
|
+
|
|
30
|
+
model SubscriberPaymentMethod {
|
|
31
|
+
id String @id @default(uuid())
|
|
32
|
+
subscriberId String
|
|
33
|
+
|
|
34
|
+
// The account the references below belong to, and its provider when the
|
|
35
|
+
// payment method was confirmed.
|
|
36
|
+
gatewayAccount String
|
|
37
|
+
provider String
|
|
38
|
+
customerRef String
|
|
39
|
+
paymentMethodRef String
|
|
40
|
+
|
|
41
|
+
// What tells the payment method apart, never enough to pay with it.
|
|
42
|
+
type String // card | sepa_debit
|
|
43
|
+
brand String?
|
|
44
|
+
last4 String
|
|
45
|
+
expiryMonth Int?
|
|
46
|
+
expiryYear Int?
|
|
47
|
+
country String? // ISO 3166-1 alpha-2
|
|
48
|
+
bankCode String?
|
|
49
|
+
mandateReference String?
|
|
50
|
+
|
|
51
|
+
status String @default("ACTIVE") // ACTIVE | REPLACED
|
|
52
|
+
confirmedAt DateTime
|
|
53
|
+
replacedAt DateTime?
|
|
54
|
+
createdAt DateTime @default(now())
|
|
55
|
+
|
|
56
|
+
subscriber Subscriber @relation(fields: [subscriberId], references: [id], onDelete: Restrict)
|
|
57
|
+
|
|
58
|
+
@@unique([gatewayAccount, paymentMethodRef])
|
|
59
|
+
@@index([subscriberId, status])
|
|
60
|
+
@@index([gatewayAccount, status])
|
|
61
|
+
@@map("subscriber_payment_methods")
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// A change of payment method a tenant started: the gateway session opened for
|
|
65
|
+
// the subscriber. A confirmation is recorded only against the setup whose
|
|
66
|
+
// account, session and subscriber it names, and completes it on the same
|
|
67
|
+
// transaction — so a callback naming another subscriber than the session was
|
|
68
|
+
// opened for changes nobody's payment method. A setup nobody finishes stays
|
|
69
|
+
// open; it holds no reference that could be charged.
|
|
70
|
+
model SubscriberPaymentMethodSetup {
|
|
71
|
+
id String @id @default(uuid())
|
|
72
|
+
subscriberId String
|
|
73
|
+
gatewayAccount String
|
|
74
|
+
sessionRef String
|
|
75
|
+
customerRef String
|
|
76
|
+
startedAt DateTime @default(now())
|
|
77
|
+
completedAt DateTime?
|
|
78
|
+
|
|
79
|
+
subscriber Subscriber @relation(fields: [subscriberId], references: [id], onDelete: Restrict)
|
|
80
|
+
|
|
81
|
+
@@unique([gatewayAccount, sessionRef])
|
|
82
|
+
@@index([subscriberId])
|
|
83
|
+
@@map("subscriber_payment_method_setups")
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// PaymentEventLog — every payment gateway event, handled once.
|
|
87
|
+
// Gateways deliver at least once. An event is claimed on the transaction that
|
|
88
|
+
// writes what it changes, so a retry after a rollback is handled and a retry
|
|
89
|
+
// after a commit finds the claim. `eventId` is unique only within the gateway
|
|
90
|
+
// account that sent it — two accounts side by side never take each other's
|
|
91
|
+
// event for a duplicate. `status` holds the event's kind as SaaSiCat read it
|
|
92
|
+
// (`payment-method-confirmed`, …) and `payload` a summary that names no person.
|
|
93
|
+
// One session is confirmed once, however many events report it: a partial
|
|
94
|
+
// unique index in sql/constraints.postgres.sql, where the reasoning is.
|
|
95
|
+
model PaymentEventLog {
|
|
96
|
+
id String @id @default(cuid())
|
|
97
|
+
gatewayAccount String
|
|
98
|
+
eventId String
|
|
99
|
+
provider String
|
|
100
|
+
sessionId String?
|
|
101
|
+
status String
|
|
102
|
+
payload Json?
|
|
103
|
+
processedAt DateTime @default(now())
|
|
104
|
+
|
|
105
|
+
@@unique([gatewayAccount, eventId])
|
|
106
|
+
@@index([sessionId])
|
|
107
|
+
@@index([status, processedAt])
|
|
108
|
+
}
|
|
@@ -11,7 +11,7 @@ related:
|
|
|
11
11
|
# Prisma Fragments
|
|
12
12
|
|
|
13
13
|
Prisma-DSL rendering of the SaaSiCat data model, **derived from the normative
|
|
14
|
-
sources**: the logical data model in [`docs/data-model.md`](../../../docs/data-model.md)
|
|
14
|
+
sources**: the logical data model in [`docs/explanation/data-model.md`](../../../docs/explanation/data-model.md)
|
|
15
15
|
and the PostgreSQL artifacts in [`../sql/`](../sql/)
|
|
16
16
|
(`reference-schema.postgres.sql` is generated FROM these fragments via
|
|
17
17
|
`pnpm run gen:sql`; `constraints.postgres.sql` carries the invariants Prisma
|
|
@@ -28,19 +28,22 @@ 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`, `
|
|
34
|
-
| [`02-promo-code.prisma`](02-promo-code.prisma) | `PromoCode`, `PromoCodeRedemption`, `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
|
|
42
|
-
| [`10-super-admin.prisma`](10-super-admin.prisma) | `SuperAdminUser`, `SuperAdminMfa`
|
|
43
|
-
| [`11-subscription-bundle.prisma`](11-subscription-bundle.prisma) | `SubscriptionBundle`
|
|
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` |
|
|
44
47
|
|
|
45
48
|
## How the consumer uses the fragments
|
|
46
49
|
|
|
@@ -82,16 +85,24 @@ Fields such as `tenantId String` and `userId String?` remain as plain
|
|
|
82
85
|
string columns in the fragments; the corresponding `@relation` is left as a
|
|
83
86
|
comment. The consumer enables them using their own `Tenant`/`User` model names.
|
|
84
87
|
|
|
88
|
+
Two models deliberately carry no such pointer: `SubscriptionContract` and
|
|
89
|
+
`SubscriberTenant`. A contract belongs to its subscriber and outlives the tenant
|
|
90
|
+
it was concluded for, so its `tenantId` is a trace — a cascade from the tenant
|
|
91
|
+
would delete the tax record with it, and a restriction would keep the tenant
|
|
92
|
+
from ever being deleted.
|
|
93
|
+
|
|
85
94
|
### 3. Table names (`@@map`) are canonical
|
|
86
95
|
|
|
87
|
-
`subscriptions`, `
|
|
88
|
-
`
|
|
89
|
-
`
|
|
90
|
-
`capability_catalog_entries`, `feature_catalog_entries`,
|
|
96
|
+
`subscriptions`, `checkout_offers`, `plans`, `plan_versions`, `promo_codes`,
|
|
97
|
+
`promo_code_redemptions`, `promo_code_holds`, `promo_code_validation_logs`, `audit_logs`, `bundles`,
|
|
98
|
+
`bundle_versions`, `capability_catalog_entries`, `feature_catalog_entries`,
|
|
91
99
|
`quota_catalog_entries`, `marketing_projections`, `marketing_settings`,
|
|
92
100
|
`promotions`, `subscription_contracts`, `contract_line_items`,
|
|
93
|
-
`
|
|
94
|
-
`
|
|
101
|
+
`super_admin_users`, `super_admin_mfa`, `subscription_bundles`, `subscribers`,
|
|
102
|
+
`subscriber_tenants`, `subscriber_corrections`, `subscriber_payment_methods`,
|
|
103
|
+
`subscriber_payment_method_setups`.
|
|
104
|
+
`PendingRegistration` and `PaymentEventLog` carry no `@@map` and keep Prisma's
|
|
105
|
+
default names, `"PendingRegistration"` and `"PaymentEventLog"`.
|
|
95
106
|
Please do **not change** them — otherwise platform migration scripts and the
|
|
96
107
|
`@saasicat/cli` commands that rely on these names will break.
|
|
97
108
|
|
|
@@ -124,5 +135,12 @@ CREATE UNIQUE INDEX plan_versions_draft_per_plan
|
|
|
124
135
|
- **No add-on tables (#49)** — `subscription_addons`,
|
|
125
136
|
`unit_addon_versions`, `feature_addon_versions` are not a
|
|
126
137
|
sales surface; only plan versions + bundles are sold.
|
|
127
|
-
- **
|
|
128
|
-
|
|
138
|
+
- **The application's own invoicing** — invoices to the people it sells to, fees
|
|
139
|
+
it collects from its members, their bank details — belongs in the schema of the
|
|
140
|
+
consuming app. The subscription business is the platform's: subscribers, their
|
|
141
|
+
invoices and their payments are decided in
|
|
142
|
+
[ADR 0012](../../../docs/explanation/adr/0012-the-subscriber-owns-the-commercial-record.md),
|
|
143
|
+
and every model added for them carries `Subscription` or `Subscriber` in its name,
|
|
144
|
+
which keeps it clear of the names applications use for their own invoicing. A
|
|
145
|
+
prefix cannot rule out a name nobody has seen, so `saasicat schema check` reports
|
|
146
|
+
an application model that carries a platform model's name with a different shape.
|
|
@@ -76,11 +76,17 @@
|
|
|
76
76
|
"properties": {
|
|
77
77
|
"source": {
|
|
78
78
|
"type": "string",
|
|
79
|
-
"description": "
|
|
79
|
+
"description": "Where the plans and features were read from: 'database' for the ones an operator publishes, 'given' for a catalogue passed in at start"
|
|
80
80
|
},
|
|
81
81
|
"hash": { "type": "string", "pattern": "^sha256-[A-Za-z0-9+/=_-]{32,}$" },
|
|
82
82
|
"currency": { "type": "string", "pattern": "^[A-Z]{3}$" },
|
|
83
|
-
"vatRate": {
|
|
83
|
+
"vatRate": {
|
|
84
|
+
"type": "number",
|
|
85
|
+
"minimum": 0,
|
|
86
|
+
"maximum": 100,
|
|
87
|
+
"not": { "exclusiveMinimum": 0, "exclusiveMaximum": 1 },
|
|
88
|
+
"description": "VAT rate as a percentage: 19 means 19 %."
|
|
89
|
+
},
|
|
84
90
|
"features": {
|
|
85
91
|
"type": "array",
|
|
86
92
|
"items": {
|
|
@@ -134,7 +140,8 @@
|
|
|
134
140
|
"bundles": { "$ref": "#/$defs/StandardPageDef" },
|
|
135
141
|
"marketingCatalog": { "$ref": "#/$defs/StandardPageDef" },
|
|
136
142
|
"platformEmail": { "$ref": "#/$defs/StandardPageDef" },
|
|
137
|
-
"platformEmailHistory": { "$ref": "#/$defs/StandardPageDef" }
|
|
143
|
+
"platformEmailHistory": { "$ref": "#/$defs/StandardPageDef" },
|
|
144
|
+
"settings": { "$ref": "#/$defs/StandardPageDef" }
|
|
138
145
|
}
|
|
139
146
|
},
|
|
140
147
|
"projectPages": {
|