@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.
@@ -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.15
32
+ version: 1.0.0-rc.17
33
33
  description: |
34
34
  Read and write operations for platform administration:
35
35
  tenants, users, subscriptions, promo codes, audit log,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saasicat/spec",
3
- "version": "1.0.0-rc.15",
3
+ "version": "1.0.0-rc.17",
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",
@@ -1,5 +1,5 @@
1
1
  // =============================================================================
2
- // SaaS-Platform Prisma fragment: Subscription + Payment method
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
- // tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
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 + PaymentEventLog
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 after successful payment does it become
11
- // User + Tenant + Subscription until then the record deliberately stays
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, PaymentEventLog).
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
- // after payment (step 4).
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 Json?
68
- billingCycle String?
69
- appliedPromoCode String?
70
- checkoutSessionId String?
71
- checkoutStartedAt DateTime?
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`, `SubscriptionPaymentMethod`, `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`, `PaymentEventLog` + `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` |
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`, `subscription_payment_methods`, `checkout_offers`, `plans`,
89
- `plan_versions`, `promo_codes`, `promo_code_redemptions`,
90
- `promo_code_validation_logs`, `audit_logs`, `bundles`, `bundle_versions`,
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
- `pending_registrations`, `payment_event_logs`, `super_admin_users`,
95
- `super_admin_mfa`, `subscription_bundles`.
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"],