@saasicat/spec 1.0.0-rc.15 → 1.0.0-rc.17
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/admin-api.openapi.yaml +1 -1
- package/package.json +1 -1
- package/prisma-fragments/01-subscription.prisma +1 -43
- package/prisma-fragments/08-subscription-contract.prisma +21 -1
- package/prisma-fragments/09-pending-registration.prisma +32 -27
- package/prisma-fragments/13-subscriber.prisma +94 -0
- package/prisma-fragments/14-payments.prisma +100 -0
- package/prisma-fragments/README.md +30 -20
- package/schemas/plan-catalog.schema.json +115 -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-settings-change-carries-its-order.postgres.sql +14 -2
- package/sql/1.0-the-applied-settings-are-recorded.postgres.sql +9 -1
- package/sql/constraints.postgres.sql +47 -0
- package/sql/reference-schema.postgres.sql +213 -48
package/admin-api.openapi.yaml
CHANGED
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// =============================================================================
|
|
2
|
-
// SaaS-Platform Prisma fragment: Subscription +
|
|
2
|
+
// SaaS-Platform Prisma fragment: Subscription + CheckoutOffer
|
|
3
3
|
// =============================================================================
|
|
4
4
|
//
|
|
5
5
|
// REFERENCE SNIPPET — not a standalone schema. Consumers copy
|
|
@@ -39,14 +39,6 @@ enum SubscriptionStatus {
|
|
|
39
39
|
PENDING_SALES
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
enum SubscriptionPaymentType {
|
|
43
|
-
CARD
|
|
44
|
-
SEPA
|
|
45
|
-
PAYPAL
|
|
46
|
-
KLARNA
|
|
47
|
-
INVOICE
|
|
48
|
-
}
|
|
49
|
-
|
|
50
42
|
// -----------------------------------------------------------------------------
|
|
51
43
|
// Subscription — one per Tenant. Binds to PlanVersion (see 03-plan-versions).
|
|
52
44
|
// -----------------------------------------------------------------------------
|
|
@@ -140,7 +132,6 @@ model Subscription {
|
|
|
140
132
|
|
|
141
133
|
// Relations — consumer must define `Tenant` and enable the relation.
|
|
142
134
|
// tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
|
|
143
|
-
paymentMethod SubscriptionPaymentMethod?
|
|
144
135
|
promoRedemption PromoCodeRedemption? // see 02-promo-code.prisma
|
|
145
136
|
planVersion PlanVersion @relation("SubscriptionPlanVersion", fields: [planVersionId], references: [id])
|
|
146
137
|
pendingPlanVersion PlanVersion? @relation("SubscriptionPendingPlanVersion", fields: [pendingPlanVersionId], references: [id])
|
|
@@ -154,39 +145,6 @@ model Subscription {
|
|
|
154
145
|
@@map("subscriptions")
|
|
155
146
|
}
|
|
156
147
|
|
|
157
|
-
// -----------------------------------------------------------------------------
|
|
158
|
-
// SubscriptionPaymentMethod — masked payment data per Subscription.
|
|
159
|
-
// -----------------------------------------------------------------------------
|
|
160
|
-
|
|
161
|
-
model SubscriptionPaymentMethod {
|
|
162
|
-
id String @id @default(uuid())
|
|
163
|
-
subscriptionId String @unique
|
|
164
|
-
type SubscriptionPaymentType
|
|
165
|
-
|
|
166
|
-
// Card (masked)
|
|
167
|
-
cardName String?
|
|
168
|
-
cardBrand String?
|
|
169
|
-
cardLast4 String?
|
|
170
|
-
cardExp String?
|
|
171
|
-
|
|
172
|
-
// SEPA (masked)
|
|
173
|
-
ibanLast4 String?
|
|
174
|
-
ibanName String?
|
|
175
|
-
|
|
176
|
-
// PayPal
|
|
177
|
-
paypalEmail String?
|
|
178
|
-
|
|
179
|
-
// Klarna
|
|
180
|
-
klarnaPlan String? // "invoice" | "instalments"
|
|
181
|
-
|
|
182
|
-
createdAt DateTime @default(now())
|
|
183
|
-
updatedAt DateTime @updatedAt
|
|
184
|
-
|
|
185
|
-
subscription Subscription @relation(fields: [subscriptionId], references: [id], onDelete: Cascade)
|
|
186
|
-
|
|
187
|
-
@@map("subscription_payment_methods")
|
|
188
|
-
}
|
|
189
|
-
|
|
190
148
|
// -----------------------------------------------------------------------------
|
|
191
149
|
// CheckoutOffer — immutable package snapshot from the website through to the
|
|
192
150
|
// Subscription (METAMODELL §17a).
|
|
@@ -8,6 +8,12 @@
|
|
|
8
8
|
// V3 rule: billing and entitlement must not depend on live-mutable catalog
|
|
9
9
|
// tables. `SubscriptionContract` and `ContractLineItem` therefore store full
|
|
10
10
|
// snapshots. Catalog FKs are optional and only audit/trace references.
|
|
11
|
+
//
|
|
12
|
+
// A contract belongs to its subscriber (13-subscriber.prisma), not to the
|
|
13
|
+
// tenant, and outlives the tenant (ADR 0012). `tenantId` is kept as a trace of
|
|
14
|
+
// the tenant it was concluded for, and deliberately has no relation to the
|
|
15
|
+
// application's `Tenant` model: a cascade there deletes the tax record with the
|
|
16
|
+
// tenant, and a restriction keeps a tenant from ever being deleted.
|
|
11
17
|
|
|
12
18
|
enum SubscriptionContractStatus {
|
|
13
19
|
active
|
|
@@ -28,6 +34,19 @@ model SubscriptionContract {
|
|
|
28
34
|
id String @id @default(uuid())
|
|
29
35
|
tenantId String
|
|
30
36
|
|
|
37
|
+
// Who the contract is between, copied on the day it is concluded: the
|
|
38
|
+
// subscriber as its record stood, and the issuer as `config/saas.yaml`
|
|
39
|
+
// named it — null where it named none. Neither copy follows a later change
|
|
40
|
+
// to the subscriber or the configuration. The invoice email is not copied:
|
|
41
|
+
// it says how the party is reached, not who it is.
|
|
42
|
+
subscriberId String
|
|
43
|
+
subscriberSnapshot Json
|
|
44
|
+
issuerSnapshot Json?
|
|
45
|
+
// The copies were made by the migration that attached contracts concluded
|
|
46
|
+
// before subscribers existed, not at conclusion, and are never presented as
|
|
47
|
+
// what was agreed.
|
|
48
|
+
partiesMigrated Boolean @default(false)
|
|
49
|
+
|
|
31
50
|
status SubscriptionContractStatus @default(active)
|
|
32
51
|
effectiveFrom DateTime
|
|
33
52
|
effectiveUntil DateTime?
|
|
@@ -50,9 +69,10 @@ model SubscriptionContract {
|
|
|
50
69
|
createdAt DateTime @default(now())
|
|
51
70
|
updatedAt DateTime @updatedAt
|
|
52
71
|
|
|
53
|
-
|
|
72
|
+
subscriber Subscriber @relation(fields: [subscriberId], references: [id], onDelete: Restrict)
|
|
54
73
|
|
|
55
74
|
@@index([tenantId, status, effectiveFrom])
|
|
75
|
+
@@index([subscriberId])
|
|
56
76
|
@@index([status])
|
|
57
77
|
@@index([originalOfferId])
|
|
58
78
|
@@map("subscription_contracts")
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// =============================================================================
|
|
2
|
-
// SaaS Platform Prisma fragment: PendingRegistration
|
|
2
|
+
// SaaS Platform Prisma fragment: PendingRegistration
|
|
3
3
|
// =============================================================================
|
|
4
4
|
//
|
|
5
5
|
// REFERENCE SNIPPET — see 01-subscription.prisma for conventions.
|
|
@@ -7,8 +7,9 @@
|
|
|
7
7
|
// Persistence of the multi-step registration and onboarding flow
|
|
8
8
|
// (`PendingRegistrationService`). A PendingRegistration holds the
|
|
9
9
|
// intermediate state between step 1 (capturing sign-up data) and the final
|
|
10
|
-
// activation (step 4: payment). Only
|
|
11
|
-
//
|
|
10
|
+
// activation (step 4: billing address and payment method). Only once the
|
|
11
|
+
// payment gateway confirmed the payment method does it become User + Tenant +
|
|
12
|
+
// Subscriber + Subscription — until then the record deliberately stays
|
|
12
13
|
// without a foreign key to consumer models.
|
|
13
14
|
//
|
|
14
15
|
// Unlike the other fragments, WITHOUT `@@map`: the model was adopted back from
|
|
@@ -16,12 +17,14 @@
|
|
|
16
17
|
// default names — a subsequent `@@map` would only force a table rename.
|
|
17
18
|
//
|
|
18
19
|
// Contract: @saasicat/core (src/registration.types.ts) (PendingRegistration,
|
|
19
|
-
// PendingRegistrationRepository
|
|
20
|
+
// PendingRegistrationRepository). The gateway events a sign-up's
|
|
21
|
+
// confirmation is claimed in are `PaymentEventLog`, in
|
|
22
|
+
// 14-payments.prisma.
|
|
20
23
|
// Service logic: @saasicat/nest/registration (PendingRegistrationService).
|
|
21
24
|
|
|
22
25
|
// Multi-step registration flow: status of a PendingRegistration
|
|
23
26
|
// between sign-up-data capture (step 1) and final activation
|
|
24
|
-
//
|
|
27
|
+
// once the payment method is confirmed (step 4).
|
|
25
28
|
enum RegistrationStatus {
|
|
26
29
|
PENDING_EMAIL_VERIFICATION
|
|
27
30
|
EMAIL_VERIFIED
|
|
@@ -64,11 +67,29 @@ model PendingRegistration {
|
|
|
64
67
|
// - billingCycle: 'MONTHLY' | 'YEARLY' — determines the price calculation.
|
|
65
68
|
// - appliedPromoCode: last-applied code (plaintext, only for UI display;
|
|
66
69
|
// validation runs fresh every time via PromoCodesService).
|
|
67
|
-
configJson
|
|
68
|
-
billingCycle
|
|
69
|
-
appliedPromoCode
|
|
70
|
-
|
|
71
|
-
|
|
70
|
+
configJson Json?
|
|
71
|
+
billingCycle String?
|
|
72
|
+
appliedPromoCode String?
|
|
73
|
+
|
|
74
|
+
// Billing details (step 4), which the subscriber is created with. The
|
|
75
|
+
// address is required before a payment method is set up; the tax
|
|
76
|
+
// identifiers stay optional.
|
|
77
|
+
addressLine1 String?
|
|
78
|
+
addressLine2 String?
|
|
79
|
+
postalCode String?
|
|
80
|
+
city String?
|
|
81
|
+
country String? // ISO 3166-1 alpha-2
|
|
82
|
+
vatId String?
|
|
83
|
+
taxNumber String?
|
|
84
|
+
|
|
85
|
+
// The payment gateway's session for the payment method. A session id is
|
|
86
|
+
// unique only within the gateway account that opened it, so the account
|
|
87
|
+
// is kept beside it; the customer the gateway created is reused when the
|
|
88
|
+
// step is repeated.
|
|
89
|
+
checkoutSessionId String?
|
|
90
|
+
checkoutGatewayAccount String?
|
|
91
|
+
gatewayCustomerRef String?
|
|
92
|
+
checkoutStartedAt DateTime?
|
|
72
93
|
|
|
73
94
|
expiresAt DateTime
|
|
74
95
|
createdAt DateTime @default(now())
|
|
@@ -76,21 +97,5 @@ model PendingRegistration {
|
|
|
76
97
|
|
|
77
98
|
@@index([status, expiresAt])
|
|
78
99
|
@@index([tenantSlug])
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
// PaymentEventLog — idempotency key for payment webhooks.
|
|
82
|
-
// Prevents a doubly delivered webhook (Stripe at-least-once) from triggering
|
|
83
|
-
// the final activation more than once. `eventId` is the provider event ID
|
|
84
|
-
// (Stripe `event.id`), `tryClaim` uses a @unique INSERT as race protection.
|
|
85
|
-
model PaymentEventLog {
|
|
86
|
-
id String @id @default(cuid())
|
|
87
|
-
eventId String @unique
|
|
88
|
-
provider String
|
|
89
|
-
sessionId String?
|
|
90
|
-
status String
|
|
91
|
-
payload Json?
|
|
92
|
-
processedAt DateTime @default(now())
|
|
93
|
-
|
|
94
|
-
@@index([sessionId])
|
|
95
|
-
@@index([status, processedAt])
|
|
100
|
+
@@index([checkoutGatewayAccount, checkoutSessionId])
|
|
96
101
|
}
|
|
@@ -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,100 @@
|
|
|
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
|
+
// Contract: @saasicat/core (SubscriberPaymentMethodRepository, PaymentEventLog).
|
|
19
|
+
// Both are needed wherever payment methods are taken — self-registration or
|
|
20
|
+
// not — because every gateway callback is claimed in `PaymentEventLog`.
|
|
21
|
+
|
|
22
|
+
model SubscriberPaymentMethod {
|
|
23
|
+
id String @id @default(uuid())
|
|
24
|
+
subscriberId String
|
|
25
|
+
|
|
26
|
+
// The account the references below belong to, and its provider when the
|
|
27
|
+
// payment method was confirmed.
|
|
28
|
+
gatewayAccount String
|
|
29
|
+
provider String
|
|
30
|
+
customerRef String
|
|
31
|
+
paymentMethodRef String
|
|
32
|
+
|
|
33
|
+
// What tells the payment method apart, never enough to pay with it.
|
|
34
|
+
type String // card | sepa_debit
|
|
35
|
+
brand String?
|
|
36
|
+
last4 String
|
|
37
|
+
expiryMonth Int?
|
|
38
|
+
expiryYear Int?
|
|
39
|
+
country String? // ISO 3166-1 alpha-2
|
|
40
|
+
bankCode String?
|
|
41
|
+
mandateReference String?
|
|
42
|
+
|
|
43
|
+
status String @default("ACTIVE") // ACTIVE | REPLACED
|
|
44
|
+
confirmedAt DateTime
|
|
45
|
+
replacedAt DateTime?
|
|
46
|
+
createdAt DateTime @default(now())
|
|
47
|
+
|
|
48
|
+
subscriber Subscriber @relation(fields: [subscriberId], references: [id], onDelete: Restrict)
|
|
49
|
+
|
|
50
|
+
@@unique([gatewayAccount, paymentMethodRef])
|
|
51
|
+
@@index([subscriberId, status])
|
|
52
|
+
@@index([gatewayAccount, status])
|
|
53
|
+
@@map("subscriber_payment_methods")
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// A change of payment method a tenant started: the gateway session opened for
|
|
57
|
+
// the subscriber. A confirmation is recorded only against the setup whose
|
|
58
|
+
// account, session and subscriber it names, and completes it on the same
|
|
59
|
+
// transaction — so a callback naming another subscriber than the session was
|
|
60
|
+
// opened for changes nobody's payment method. A setup nobody finishes stays
|
|
61
|
+
// open; it holds no reference that could be charged.
|
|
62
|
+
model SubscriberPaymentMethodSetup {
|
|
63
|
+
id String @id @default(uuid())
|
|
64
|
+
subscriberId String
|
|
65
|
+
gatewayAccount String
|
|
66
|
+
sessionRef String
|
|
67
|
+
customerRef String
|
|
68
|
+
startedAt DateTime @default(now())
|
|
69
|
+
completedAt DateTime?
|
|
70
|
+
|
|
71
|
+
subscriber Subscriber @relation(fields: [subscriberId], references: [id], onDelete: Restrict)
|
|
72
|
+
|
|
73
|
+
@@unique([gatewayAccount, sessionRef])
|
|
74
|
+
@@index([subscriberId])
|
|
75
|
+
@@map("subscriber_payment_method_setups")
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// PaymentEventLog — every payment gateway event, handled once.
|
|
79
|
+
// Gateways deliver at least once. An event is claimed on the transaction that
|
|
80
|
+
// writes what it changes, so a retry after a rollback is handled and a retry
|
|
81
|
+
// after a commit finds the claim. `eventId` is unique only within the gateway
|
|
82
|
+
// account that sent it — two accounts side by side never take each other's
|
|
83
|
+
// event for a duplicate. `status` holds the event's kind as SaaSiCat read it
|
|
84
|
+
// (`payment-method-confirmed`, …) and `payload` a summary that names no person.
|
|
85
|
+
// One session is confirmed once, however many events report it: a partial
|
|
86
|
+
// unique index in sql/constraints.postgres.sql, where the reasoning is.
|
|
87
|
+
model PaymentEventLog {
|
|
88
|
+
id String @id @default(cuid())
|
|
89
|
+
gatewayAccount String
|
|
90
|
+
eventId String
|
|
91
|
+
provider String
|
|
92
|
+
sessionId String?
|
|
93
|
+
status String
|
|
94
|
+
payload Json?
|
|
95
|
+
processedAt DateTime @default(now())
|
|
96
|
+
|
|
97
|
+
@@unique([gatewayAccount, eventId])
|
|
98
|
+
@@index([sessionId])
|
|
99
|
+
@@index([status, processedAt])
|
|
100
|
+
}
|
|
@@ -28,20 +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`
|
|
44
|
-
| [`12-applied-settings.prisma`](12-applied-settings.prisma) | `AppliedSettings`, `SettingsChange`
|
|
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`, `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` |
|
|
45
47
|
|
|
46
48
|
## How the consumer uses the fragments
|
|
47
49
|
|
|
@@ -83,16 +85,24 @@ Fields such as `tenantId String` and `userId String?` remain as plain
|
|
|
83
85
|
string columns in the fragments; the corresponding `@relation` is left as a
|
|
84
86
|
comment. The consumer enables them using their own `Tenant`/`User` model names.
|
|
85
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
|
+
|
|
86
94
|
### 3. Table names (`@@map`) are canonical
|
|
87
95
|
|
|
88
|
-
`subscriptions`, `
|
|
89
|
-
`
|
|
90
|
-
`
|
|
91
|
-
`capability_catalog_entries`, `feature_catalog_entries`,
|
|
96
|
+
`subscriptions`, `checkout_offers`, `plans`, `plan_versions`, `promo_codes`,
|
|
97
|
+
`promo_code_redemptions`, `promo_code_validation_logs`, `audit_logs`, `bundles`,
|
|
98
|
+
`bundle_versions`, `capability_catalog_entries`, `feature_catalog_entries`,
|
|
92
99
|
`quota_catalog_entries`, `marketing_projections`, `marketing_settings`,
|
|
93
100
|
`promotions`, `subscription_contracts`, `contract_line_items`,
|
|
94
|
-
`
|
|
95
|
-
`
|
|
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"`.
|
|
96
106
|
Please do **not change** them — otherwise platform migration scripts and the
|
|
97
107
|
`@saasicat/cli` commands that rely on these names will break.
|
|
98
108
|
|
|
@@ -137,6 +137,98 @@
|
|
|
137
137
|
}
|
|
138
138
|
}
|
|
139
139
|
},
|
|
140
|
+
"issuer": {
|
|
141
|
+
"type": "object",
|
|
142
|
+
"additionalProperties": false,
|
|
143
|
+
"required": ["legalName"],
|
|
144
|
+
"description": "The legal entity on the operator's side of every contract this installation concludes, and later of every invoice it issues. A contract copies it on the day it is concluded, so the contract keeps naming its counterparty after this block changes. Optional for now: a contract concluded while it is absent records that no issuer was named.",
|
|
145
|
+
"properties": {
|
|
146
|
+
"legalName": {
|
|
147
|
+
"type": "string",
|
|
148
|
+
"minLength": 1,
|
|
149
|
+
"description": "The registered name, legal form included, as a contract names the party (e.g. \"Example Software GmbH\")."
|
|
150
|
+
},
|
|
151
|
+
"addressLine1": {
|
|
152
|
+
"type": "string",
|
|
153
|
+
"minLength": 1,
|
|
154
|
+
"description": "Street and number."
|
|
155
|
+
},
|
|
156
|
+
"addressLine2": {
|
|
157
|
+
"type": "string",
|
|
158
|
+
"minLength": 1,
|
|
159
|
+
"description": "A second address line, such as a building or a c/o."
|
|
160
|
+
},
|
|
161
|
+
"postalCode": {
|
|
162
|
+
"type": "string",
|
|
163
|
+
"minLength": 1
|
|
164
|
+
},
|
|
165
|
+
"city": {
|
|
166
|
+
"type": "string",
|
|
167
|
+
"minLength": 1
|
|
168
|
+
},
|
|
169
|
+
"country": {
|
|
170
|
+
"type": "string",
|
|
171
|
+
"pattern": "^[A-Z]{2}$",
|
|
172
|
+
"description": "ISO 3166-1 alpha-2 country code, e.g. DE."
|
|
173
|
+
},
|
|
174
|
+
"vatId": {
|
|
175
|
+
"type": "string",
|
|
176
|
+
"minLength": 1,
|
|
177
|
+
"description": "VAT identification number, e.g. DE123456789."
|
|
178
|
+
},
|
|
179
|
+
"taxNumber": {
|
|
180
|
+
"type": "string",
|
|
181
|
+
"minLength": 1,
|
|
182
|
+
"description": "The tax number the issuer's tax office assigned, where it is stated beside or instead of the VAT identification number."
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
},
|
|
186
|
+
"subscribers": {
|
|
187
|
+
"type": "object",
|
|
188
|
+
"additionalProperties": false,
|
|
189
|
+
"description": "How the parties this installation concludes contracts with are numbered.",
|
|
190
|
+
"properties": {
|
|
191
|
+
"customerNumberPrefix": {
|
|
192
|
+
"type": "string",
|
|
193
|
+
"pattern": "^[A-Za-z0-9._/-]{1,16}$",
|
|
194
|
+
"description": "Put in front of every customer number assigned from the next start on: `K-` gives K-10001. A customer number keeps the prefix it was assigned with, so changing this renumbers nobody. Omitted, a customer number is the number alone."
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
"payments": {
|
|
199
|
+
"type": "object",
|
|
200
|
+
"additionalProperties": false,
|
|
201
|
+
"required": ["accounts", "returnUrlOrigins"],
|
|
202
|
+
"description": "The payment gateway accounts this installation takes payment methods through. SaaSiCat keeps the gateway's reference to a payment method and its masked details, never a card number or an IBAN. The keys and the webhook secret of each account are bound in the application's code from the environment and never written here.",
|
|
203
|
+
"properties": {
|
|
204
|
+
"newPaymentMethods": {
|
|
205
|
+
"type": "string",
|
|
206
|
+
"pattern": "^[a-z0-9][a-z0-9-]{0,39}$",
|
|
207
|
+
"description": "The account a new payment method is taken at, at sign-up and when a tenant changes its payment method. It names one of `accounts`, which then has to list its `methods`. Omitted, no new payment method is taken, and the accounts listed stay for the references they hold."
|
|
208
|
+
},
|
|
209
|
+
"returnUrlOrigins": {
|
|
210
|
+
"type": "array",
|
|
211
|
+
"minItems": 1,
|
|
212
|
+
"uniqueItems": true,
|
|
213
|
+
"items": {
|
|
214
|
+
"type": "string",
|
|
215
|
+
"pattern": "^https?://[^/?#\\s]+$"
|
|
216
|
+
},
|
|
217
|
+
"description": "The origins a gateway's form may send a person back to, e.g. `https://app.example.com`: the scheme, the host and a port, no path. A sign-up or a tenant naming a success or cancel URL at any other origin is refused, so the operator's own payment form cannot be made to forward somebody to a page of a stranger's choosing."
|
|
218
|
+
},
|
|
219
|
+
"accounts": {
|
|
220
|
+
"type": "object",
|
|
221
|
+
"minProperties": 1,
|
|
222
|
+
"propertyNames": {
|
|
223
|
+
"pattern": "^[a-z0-9][a-z0-9-]{0,39}$"
|
|
224
|
+
},
|
|
225
|
+
"additionalProperties": {
|
|
226
|
+
"$ref": "#/$defs/PaymentGatewayAccount"
|
|
227
|
+
},
|
|
228
|
+
"description": "Every gateway account by the name its webhook route carries: `/webhooks/payment/<name>`. An account that still holds a payment method in use stays listed after another takes the new ones, so its callbacks keep being handled; a start that finds a stored reference to an account missing here refuses, naming the account."
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
},
|
|
140
232
|
"features": {
|
|
141
233
|
"type": "array",
|
|
142
234
|
"items": {
|
|
@@ -153,6 +245,29 @@
|
|
|
153
245
|
}
|
|
154
246
|
},
|
|
155
247
|
"$defs": {
|
|
248
|
+
"PaymentGatewayAccount": {
|
|
249
|
+
"type": "object",
|
|
250
|
+
"additionalProperties": false,
|
|
251
|
+
"required": ["provider"],
|
|
252
|
+
"description": "One account at a payment gateway: a merchant account whose keys the application binds.",
|
|
253
|
+
"properties": {
|
|
254
|
+
"provider": {
|
|
255
|
+
"type": "string",
|
|
256
|
+
"pattern": "^[a-z0-9][a-z0-9-]{0,39}$",
|
|
257
|
+
"description": "The gateway the account is at, as its adapter names itself: `stripe` for @saasicat/payment-stripe. A start refuses an account whose bound adapter names another."
|
|
258
|
+
},
|
|
259
|
+
"methods": {
|
|
260
|
+
"type": "array",
|
|
261
|
+
"minItems": 1,
|
|
262
|
+
"uniqueItems": true,
|
|
263
|
+
"items": {
|
|
264
|
+
"type": "string",
|
|
265
|
+
"enum": ["card", "sepa_debit"]
|
|
266
|
+
},
|
|
267
|
+
"description": "The payment methods a new payment method may be at this account: `card`, `sepa_debit`. Required for the account `newPaymentMethods` names and read for no other."
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
},
|
|
156
271
|
"FeatureDef": {
|
|
157
272
|
"type": "object",
|
|
158
273
|
"required": ["key"],
|