@saasicat/spec 1.0.0-rc.2 → 1.0.0-rc.20
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 +328 -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/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 +100 -0
- package/prisma-fragments/README.md +40 -22
- package/schemas/admin-manifest.schema.json +9 -2
- package/schemas/plan-catalog.schema.json +239 -18
- 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-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 +299 -77
|
@@ -4,22 +4,17 @@
|
|
|
4
4
|
"title": "PlanCatalog",
|
|
5
5
|
"description": "Language-neutral definition of a plan catalog + app identity for a SaaS app. Maintained by the consumer as a YAML/JSON file (`config/saas.yaml`) and mirrored into platform tables at backend boot.",
|
|
6
6
|
"type": "object",
|
|
7
|
-
"required": ["schemaVersion", "
|
|
7
|
+
"required": ["schemaVersion", "app", "currency", "vatRate", "tenantBilling"],
|
|
8
8
|
"additionalProperties": false,
|
|
9
9
|
"properties": {
|
|
10
10
|
"schemaVersion": {
|
|
11
11
|
"type": "integer",
|
|
12
12
|
"const": 1
|
|
13
13
|
},
|
|
14
|
-
"projectKey": {
|
|
15
|
-
"type": "string",
|
|
16
|
-
"pattern": "^[a-z][a-z0-9-]{1,30}$",
|
|
17
|
-
"description": "Unique key of the consuming project. Cross-audit marker."
|
|
18
|
-
},
|
|
19
14
|
"app": {
|
|
20
15
|
"type": "object",
|
|
21
16
|
"additionalProperties": false,
|
|
22
|
-
"description": "App identity block (branding + version). Consumed by AdminPublicBootController + AdminManifestConfigFactory.",
|
|
17
|
+
"description": "App identity block (branding + version). One installation serves one application, so this is the only place it is named. Consumed by AdminPublicBootController + AdminManifestConfigFactory.",
|
|
23
18
|
"required": ["name"],
|
|
24
19
|
"properties": {
|
|
25
20
|
"name": {
|
|
@@ -57,7 +52,56 @@
|
|
|
57
52
|
"type": "number",
|
|
58
53
|
"minimum": 0,
|
|
59
54
|
"maximum": 100,
|
|
60
|
-
"
|
|
55
|
+
"not": { "exclusiveMinimum": 0, "exclusiveMaximum": 1 },
|
|
56
|
+
"description": "VAT rate as a percentage: 19 means 19 %. Required (even when 0). Every tax rate in SaaSiCat is a percentage, so a value between 0 and 1, the shape of a fraction such as 0.19, is refused."
|
|
57
|
+
},
|
|
58
|
+
"tenantBilling": {
|
|
59
|
+
"type": "object",
|
|
60
|
+
"additionalProperties": false,
|
|
61
|
+
"description": "Commercial settings for the tenant-facing self-service routes. Required, and required member by member: every one of these has a money or a legal consequence, and a value left out is still a decision — just an invisible one. The file is read at boot, so a change lands on the next restart.",
|
|
62
|
+
"required": ["cancellationNoticeDays", "selfServiceBlockedPlans"],
|
|
63
|
+
"properties": {
|
|
64
|
+
"cancellationNoticeDays": {
|
|
65
|
+
"type": "object",
|
|
66
|
+
"additionalProperties": false,
|
|
67
|
+
"description": "Days of notice before a term ends, one number per rhythm. A cancellation declared after the window has closed takes effect at the first period end that actually serves the notice. Two numbers rather than one because a monthly and a yearly contract cannot share a notice period: a fortnight is unusual on a year, and three months is void against a consumer on a month. No ceiling is enforced — §309 Nr. 9 BGB caps it at one month in German consumer contracts, and an installation serving businesses is not bound by that.",
|
|
68
|
+
"required": ["monthly", "yearly"],
|
|
69
|
+
"properties": {
|
|
70
|
+
"monthly": {
|
|
71
|
+
"type": "integer",
|
|
72
|
+
"minimum": 0,
|
|
73
|
+
"description": "Notice days on a monthly rhythm. 0 means there is no door to be shut out of: a cancellation on the last day still lands at the term end."
|
|
74
|
+
},
|
|
75
|
+
"yearly": {
|
|
76
|
+
"type": "integer",
|
|
77
|
+
"minimum": 0,
|
|
78
|
+
"description": "Notice days on a yearly rhythm. Raise it and the cut is hard — a declaration made after the window lands one full year later."
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
"selfServiceBlockedPlans": {
|
|
83
|
+
"type": "object",
|
|
84
|
+
"additionalProperties": false,
|
|
85
|
+
"description": "Plans a tenant may not move to or away from without talking to sales. Both lists are required and may be empty; an empty list is the explicit statement that self-service reaches every plan.",
|
|
86
|
+
"required": ["asTarget", "asSource"],
|
|
87
|
+
"properties": {
|
|
88
|
+
"asTarget": {
|
|
89
|
+
"type": "array",
|
|
90
|
+
"items": {
|
|
91
|
+
"type": "string"
|
|
92
|
+
},
|
|
93
|
+
"description": "Plan IDs that may not be selected via self-service — typically ENTERPRISE, which only a special contract activates."
|
|
94
|
+
},
|
|
95
|
+
"asSource": {
|
|
96
|
+
"type": "array",
|
|
97
|
+
"items": {
|
|
98
|
+
"type": "string"
|
|
99
|
+
},
|
|
100
|
+
"description": "Plan IDs that may not be left via self-service — typically an active special contract, whose change goes through sales."
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
61
105
|
},
|
|
62
106
|
"marketing": {
|
|
63
107
|
"type": "object",
|
|
@@ -67,26 +111,191 @@
|
|
|
67
111
|
"properties": {
|
|
68
112
|
"availableLocales": {
|
|
69
113
|
"type": "array",
|
|
70
|
-
"items": {
|
|
114
|
+
"items": {
|
|
115
|
+
"type": "string",
|
|
116
|
+
"pattern": "^[a-z]{2}(-[A-Z]{2})?$"
|
|
117
|
+
},
|
|
71
118
|
"minItems": 1,
|
|
72
119
|
"uniqueItems": true,
|
|
73
120
|
"description": "Allowed language pool the app may market. First = default locale. From it the SuperAdmin activates a subset in the marketing catalog (LocaleManager)."
|
|
74
121
|
}
|
|
75
122
|
}
|
|
76
123
|
},
|
|
124
|
+
"notifications": {
|
|
125
|
+
"type": "object",
|
|
126
|
+
"additionalProperties": false,
|
|
127
|
+
"description": "Who is told when something in this file changes between two starts. The record inside the application is written either way; mail is the addition, never the substitute. Optional: an installation of one operator who signs in daily needs nothing here.",
|
|
128
|
+
"properties": {
|
|
129
|
+
"settingsChanged": {
|
|
130
|
+
"type": "array",
|
|
131
|
+
"items": {
|
|
132
|
+
"type": "string",
|
|
133
|
+
"format": "email"
|
|
134
|
+
},
|
|
135
|
+
"uniqueItems": true,
|
|
136
|
+
"description": "Addresses mailed when the settings applied at a start differ from the ones applied at the previous start. Mailed only where an email port is bound (`adapters.email`); without one the boot log says so once and the change is recorded in the application only. An empty list names nobody."
|
|
137
|
+
}
|
|
138
|
+
}
|
|
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
|
+
"pattern": "\\S",
|
|
149
|
+
"description": "The registered name, legal form included, as a contract names the party (e.g. \"Example Software GmbH\"). Compared with the recorded one after the surrounding whitespace is taken off, so a value that is only whitespace is not a name at all."
|
|
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
|
+
"pattern": "\\S",
|
|
177
|
+
"description": "VAT identification number, e.g. DE123456789."
|
|
178
|
+
},
|
|
179
|
+
"taxNumber": {
|
|
180
|
+
"type": "string",
|
|
181
|
+
"pattern": "\\S",
|
|
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
|
+
"correctionOf": {
|
|
185
|
+
"type": "object",
|
|
186
|
+
"additionalProperties": false,
|
|
187
|
+
"required": ["reason"],
|
|
188
|
+
"description": "Declares that a changed `legalName`, `vatId` or `taxNumber` corrects the same legal entity rather than naming another one -- a misspelt name, a tax identifier that was wrong or missing, a change of name the entity went through. Without it a start that finds a different identity recorded is refused while contracts concluded under the previous one still run, because moving a contract to another legal entity is a transfer and not an edit of a setting. Name here, for every identity field the change moves, the value the installation recorded before it -- `null` where it recorded none. The declaration is needed only for the start that carries the change; a later deploy may drop it. It carries no date of its own: the settings record dates the start that applied it, and where the legal change has a date of its own it belongs in `reason`.",
|
|
189
|
+
"properties": {
|
|
190
|
+
"legalName": {
|
|
191
|
+
"type": ["string", "null"],
|
|
192
|
+
"pattern": "\\S",
|
|
193
|
+
"description": "The registered name being replaced, or `null` where none was recorded."
|
|
194
|
+
},
|
|
195
|
+
"vatId": {
|
|
196
|
+
"type": ["string", "null"],
|
|
197
|
+
"pattern": "\\S",
|
|
198
|
+
"description": "The VAT identification number being replaced, or `null` where none was recorded."
|
|
199
|
+
},
|
|
200
|
+
"taxNumber": {
|
|
201
|
+
"type": ["string", "null"],
|
|
202
|
+
"pattern": "\\S",
|
|
203
|
+
"description": "The tax number being replaced, or `null` where none was recorded."
|
|
204
|
+
},
|
|
205
|
+
"reason": {
|
|
206
|
+
"type": "string",
|
|
207
|
+
"pattern": "\\S",
|
|
208
|
+
"description": "Why the same entity now reads differently, e.g. \"Change of legal form, registered 2026-07-01\". Kept in the settings record with both value trees."
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
"subscribers": {
|
|
215
|
+
"type": "object",
|
|
216
|
+
"additionalProperties": false,
|
|
217
|
+
"description": "How the parties this installation concludes contracts with are numbered.",
|
|
218
|
+
"properties": {
|
|
219
|
+
"customerNumberPrefix": {
|
|
220
|
+
"type": "string",
|
|
221
|
+
"pattern": "^[A-Za-z0-9._/-]{1,16}$",
|
|
222
|
+
"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."
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
},
|
|
226
|
+
"payments": {
|
|
227
|
+
"type": "object",
|
|
228
|
+
"additionalProperties": false,
|
|
229
|
+
"required": ["accounts", "returnUrlOrigins"],
|
|
230
|
+
"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.",
|
|
231
|
+
"properties": {
|
|
232
|
+
"newPaymentMethods": {
|
|
233
|
+
"type": "string",
|
|
234
|
+
"pattern": "^[a-z0-9][a-z0-9-]{0,39}$",
|
|
235
|
+
"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."
|
|
236
|
+
},
|
|
237
|
+
"returnUrlOrigins": {
|
|
238
|
+
"type": "array",
|
|
239
|
+
"minItems": 1,
|
|
240
|
+
"uniqueItems": true,
|
|
241
|
+
"items": {
|
|
242
|
+
"type": "string",
|
|
243
|
+
"pattern": "^https?://[^/?#\\s]+$"
|
|
244
|
+
},
|
|
245
|
+
"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."
|
|
246
|
+
},
|
|
247
|
+
"accounts": {
|
|
248
|
+
"type": "object",
|
|
249
|
+
"minProperties": 1,
|
|
250
|
+
"propertyNames": {
|
|
251
|
+
"pattern": "^[a-z0-9][a-z0-9-]{0,39}$"
|
|
252
|
+
},
|
|
253
|
+
"additionalProperties": {
|
|
254
|
+
"$ref": "#/$defs/PaymentGatewayAccount"
|
|
255
|
+
},
|
|
256
|
+
"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."
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
},
|
|
77
260
|
"features": {
|
|
78
261
|
"type": "array",
|
|
79
|
-
"items": {
|
|
262
|
+
"items": {
|
|
263
|
+
"$ref": "#/$defs/FeatureDef"
|
|
264
|
+
},
|
|
80
265
|
"description": "Master list of all feature flags of the project. Plans may only reference keys declared here."
|
|
81
266
|
},
|
|
82
267
|
"plans": {
|
|
83
268
|
"type": "array",
|
|
84
|
-
"items": {
|
|
269
|
+
"items": {
|
|
270
|
+
"$ref": "#/$defs/PlanDef"
|
|
271
|
+
},
|
|
85
272
|
"description": "Optional. When omitted, plans come exclusively from the AdminUI / DB."
|
|
86
273
|
}
|
|
87
274
|
},
|
|
88
|
-
|
|
89
275
|
"$defs": {
|
|
276
|
+
"PaymentGatewayAccount": {
|
|
277
|
+
"type": "object",
|
|
278
|
+
"additionalProperties": false,
|
|
279
|
+
"required": ["provider"],
|
|
280
|
+
"description": "One account at a payment gateway: a merchant account whose keys the application binds.",
|
|
281
|
+
"properties": {
|
|
282
|
+
"provider": {
|
|
283
|
+
"type": "string",
|
|
284
|
+
"pattern": "^[a-z0-9][a-z0-9-]{0,39}$",
|
|
285
|
+
"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."
|
|
286
|
+
},
|
|
287
|
+
"methods": {
|
|
288
|
+
"type": "array",
|
|
289
|
+
"minItems": 1,
|
|
290
|
+
"uniqueItems": true,
|
|
291
|
+
"items": {
|
|
292
|
+
"type": "string",
|
|
293
|
+
"enum": ["card", "sepa_debit"]
|
|
294
|
+
},
|
|
295
|
+
"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."
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
},
|
|
90
299
|
"FeatureDef": {
|
|
91
300
|
"type": "object",
|
|
92
301
|
"required": ["key"],
|
|
@@ -97,8 +306,13 @@
|
|
|
97
306
|
"pattern": "^[A-Z][A-Z0-9_]+$",
|
|
98
307
|
"description": "SCREAMING_SNAKE_CASE; unique per catalog."
|
|
99
308
|
},
|
|
100
|
-
"label": {
|
|
101
|
-
|
|
309
|
+
"label": {
|
|
310
|
+
"type": "string",
|
|
311
|
+
"minLength": 1
|
|
312
|
+
},
|
|
313
|
+
"icon": {
|
|
314
|
+
"type": "string"
|
|
315
|
+
},
|
|
102
316
|
"tier": {
|
|
103
317
|
"type": "string",
|
|
104
318
|
"description": "Optional logical group. Convention: CORE / ADVANCED / PRO / BUSINESS / ENTERPRISE_ONLY."
|
|
@@ -110,7 +324,6 @@
|
|
|
110
324
|
}
|
|
111
325
|
}
|
|
112
326
|
},
|
|
113
|
-
|
|
114
327
|
"PlanDef": {
|
|
115
328
|
"type": "object",
|
|
116
329
|
"required": ["id", "quotas", "features"],
|
|
@@ -121,8 +334,13 @@
|
|
|
121
334
|
"pattern": "^[A-Z][A-Z0-9_]+$",
|
|
122
335
|
"description": "Plan ID. Freely chosen by the consumer (BASIC, STANDARD, ...). Ends up as a string in Subscription.plan."
|
|
123
336
|
},
|
|
124
|
-
"name": {
|
|
125
|
-
|
|
337
|
+
"name": {
|
|
338
|
+
"type": "string",
|
|
339
|
+
"minLength": 1
|
|
340
|
+
},
|
|
341
|
+
"tagline": {
|
|
342
|
+
"type": "string"
|
|
343
|
+
},
|
|
126
344
|
"marketed": {
|
|
127
345
|
"type": "boolean",
|
|
128
346
|
"default": true,
|
|
@@ -156,7 +374,10 @@
|
|
|
156
374
|
},
|
|
157
375
|
"features": {
|
|
158
376
|
"type": "array",
|
|
159
|
-
"items": {
|
|
377
|
+
"items": {
|
|
378
|
+
"type": "string",
|
|
379
|
+
"pattern": "^[A-Z][A-Z0-9_]+$"
|
|
380
|
+
},
|
|
160
381
|
"uniqueItems": true,
|
|
161
382
|
"description": "List of included feature keys; all must exist in features[].key."
|
|
162
383
|
}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://saasicat.dev/schemas/tenant-ledger.schema.json",
|
|
4
|
+
"title": "TenantLedgerEntry",
|
|
5
|
+
"description": "One entry in a tenant's account. Two kinds share one journal: a charge is what became due, a payment is what settled part of it. The journal is append-only — an entry is never updated, and a mistake is answered with a counter-entry rather than an edit, so the account says what happened rather than what somebody thinks today.",
|
|
6
|
+
"oneOf": [{ "$ref": "#/$defs/LedgerCharge" }, { "$ref": "#/$defs/LedgerPayment" }],
|
|
7
|
+
|
|
8
|
+
"$defs": {
|
|
9
|
+
"LedgerEntryKind": {
|
|
10
|
+
"type": "string",
|
|
11
|
+
"enum": ["charge", "payment"],
|
|
12
|
+
"description": "charge: an amount became due. payment: an amount settled."
|
|
13
|
+
},
|
|
14
|
+
|
|
15
|
+
"ChargeOrigin": {
|
|
16
|
+
"type": "string",
|
|
17
|
+
"enum": [
|
|
18
|
+
"activation",
|
|
19
|
+
"renewal",
|
|
20
|
+
"planChange",
|
|
21
|
+
"bundleBooking",
|
|
22
|
+
"credit",
|
|
23
|
+
"correction"
|
|
24
|
+
],
|
|
25
|
+
"description": "What made the charge arise. activation: the first period of a subscription. renewal: a period renewing. planChange: the prorated difference of an immediate plan change. bundleBooking: an add-on booked. credit: a decision that reduces what is owed. correction: a counter-entry reversing an earlier one, which is how the append-only journal answers a mistake."
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
"Currency": {
|
|
29
|
+
"type": "string",
|
|
30
|
+
"pattern": "^[A-Z]{3}$",
|
|
31
|
+
"description": "ISO 4217. Recorded per entry even though an installation configures one at a time, so that a row written in 2026 still means what it meant after the configured currency changes."
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
"LedgerCharge": {
|
|
35
|
+
"type": "object",
|
|
36
|
+
"title": "LedgerCharge",
|
|
37
|
+
"description": "An amount that became due. It names the period it belongs to, so the charges that make up one invoice can be found later, and it names the agreement it came from, so it can be walked back to what the tenant agreed to.",
|
|
38
|
+
"required": [
|
|
39
|
+
"id",
|
|
40
|
+
"tenantId",
|
|
41
|
+
"kind",
|
|
42
|
+
"subscriptionId",
|
|
43
|
+
"origin",
|
|
44
|
+
"originRef",
|
|
45
|
+
"periodStart",
|
|
46
|
+
"periodEnd",
|
|
47
|
+
"currency",
|
|
48
|
+
"amountNet",
|
|
49
|
+
"taxRate",
|
|
50
|
+
"taxAmount",
|
|
51
|
+
"amountGross",
|
|
52
|
+
"bookedAt",
|
|
53
|
+
"createdAt"
|
|
54
|
+
],
|
|
55
|
+
"additionalProperties": false,
|
|
56
|
+
"properties": {
|
|
57
|
+
"id": {
|
|
58
|
+
"type": "string",
|
|
59
|
+
"format": "uuid",
|
|
60
|
+
"description": "The entry's own identifier. Never shown to a customer as an invoice number: invoice numbering is sequential, gapless and legally constrained per country, and an identifier somebody has already seen on a screen cannot become one later without confusion."
|
|
61
|
+
},
|
|
62
|
+
"tenantId": { "type": "string" },
|
|
63
|
+
"kind": { "const": "charge" },
|
|
64
|
+
"subscriptionId": {
|
|
65
|
+
"type": "string",
|
|
66
|
+
"description": "The subscription the charge arose on. Part of the natural key below."
|
|
67
|
+
},
|
|
68
|
+
"origin": { "$ref": "#/$defs/ChargeOrigin" },
|
|
69
|
+
"originRef": {
|
|
70
|
+
"type": "string",
|
|
71
|
+
"minLength": 1,
|
|
72
|
+
"description": "Which thing within the origin the charge is for, so that two charges of the same origin in the same period stay apart. activation and renewal: the subscription id. planChange: the id of the contract the change concluded, which differs for every change. bundleBooking: the booking id. credit and correction: the entry they answer. Never empty — the natural key (subscriptionId, origin, originRef, periodStart) is unique, and a NULL would not collide with itself, which is the one thing the key exists to prevent."
|
|
73
|
+
},
|
|
74
|
+
"periodStart": {
|
|
75
|
+
"type": "string",
|
|
76
|
+
"format": "date-time",
|
|
77
|
+
"description": "The billing period the charge belongs to. It is the grouping key for a later invoice: a set of individually booked amounts with nothing tying them to a period leaves 'which charges go on one invoice' to guesswork."
|
|
78
|
+
},
|
|
79
|
+
"periodEnd": { "type": "string", "format": "date-time" },
|
|
80
|
+
"contractId": {
|
|
81
|
+
"type": ["string", "null"],
|
|
82
|
+
"description": "The frozen agreement the charge realises. Null where none was frozen — a subscription older than the contract model, or a credit that answers no line."
|
|
83
|
+
},
|
|
84
|
+
"contractLineItemId": {
|
|
85
|
+
"type": ["string", "null"],
|
|
86
|
+
"description": "The contract line the charge came from. Null under the same conditions as contractId."
|
|
87
|
+
},
|
|
88
|
+
"currency": { "$ref": "#/$defs/Currency" },
|
|
89
|
+
"amountNet": {
|
|
90
|
+
"type": "number",
|
|
91
|
+
"description": "Held to the currency's decimal places. Negative on a credit or a correction."
|
|
92
|
+
},
|
|
93
|
+
"taxRate": {
|
|
94
|
+
"type": "number",
|
|
95
|
+
"minimum": 0,
|
|
96
|
+
"maximum": 100,
|
|
97
|
+
"description": "The rate in percent, recorded rather than left to live in the ratio between net and gross. A ratio cannot be reproduced for a rounded gross, cannot express an exempt or reverse-charge line, and does not survive a rate change."
|
|
98
|
+
},
|
|
99
|
+
"taxAmount": {
|
|
100
|
+
"type": "number",
|
|
101
|
+
"description": "The tax contained in the charge. It closes the gap between amountNet and amountGross exactly, so the entry cannot disagree with itself."
|
|
102
|
+
},
|
|
103
|
+
"amountGross": { "type": "number" },
|
|
104
|
+
"bookedAt": {
|
|
105
|
+
"type": "string",
|
|
106
|
+
"format": "date-time",
|
|
107
|
+
"description": "When the amount became due. The account is ordered by this, not by when the row was written."
|
|
108
|
+
},
|
|
109
|
+
"createdAt": { "type": "string", "format": "date-time" }
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
"LedgerPayment": {
|
|
114
|
+
"type": "object",
|
|
115
|
+
"title": "LedgerPayment",
|
|
116
|
+
"description": "An amount that settled. What talks to a bank, a card processor or an invoice run is a consumer's adapter behind a port; this records only that the money arrived and which external reference it arrived under. It carries no tax split: a payment is a movement of money, and the net, the rate and the tax belong to the charges it settles — asking a payment for them would make an integrator write a number nobody knows, which the account would then read as a fact.",
|
|
117
|
+
"required": [
|
|
118
|
+
"id",
|
|
119
|
+
"tenantId",
|
|
120
|
+
"kind",
|
|
121
|
+
"externalReference",
|
|
122
|
+
"currency",
|
|
123
|
+
"amountGross",
|
|
124
|
+
"bookedAt",
|
|
125
|
+
"createdAt"
|
|
126
|
+
],
|
|
127
|
+
"additionalProperties": false,
|
|
128
|
+
"properties": {
|
|
129
|
+
"id": { "type": "string", "format": "uuid" },
|
|
130
|
+
"tenantId": { "type": "string" },
|
|
131
|
+
"kind": { "const": "payment" },
|
|
132
|
+
"externalReference": {
|
|
133
|
+
"type": "string",
|
|
134
|
+
"minLength": 1,
|
|
135
|
+
"description": "The payment provider's own identifier for the settlement. It is what makes a payment reconcilable against the provider, and what a second delivery of the same provider event is recognised by."
|
|
136
|
+
},
|
|
137
|
+
"settlesEntryId": {
|
|
138
|
+
"type": ["string", "null"],
|
|
139
|
+
"description": "The charge this payment settles, where the provider says which. Null for a payment on account, which reduces the balance without naming a charge."
|
|
140
|
+
},
|
|
141
|
+
"currency": { "$ref": "#/$defs/Currency" },
|
|
142
|
+
"amountGross": {
|
|
143
|
+
"type": "number",
|
|
144
|
+
"description": "What arrived. Negative on a reversal, which is a movement the other way rather than an edit of the payment it answers."
|
|
145
|
+
},
|
|
146
|
+
"bookedAt": {
|
|
147
|
+
"type": "string",
|
|
148
|
+
"format": "date-time",
|
|
149
|
+
"description": "The value date — when the money settled, which is not when the row was written."
|
|
150
|
+
},
|
|
151
|
+
"createdAt": { "type": "string", "format": "date-time" }
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
|
|
155
|
+
"TenantAccountOpenItem": {
|
|
156
|
+
"type": "object",
|
|
157
|
+
"title": "TenantAccountOpenItem",
|
|
158
|
+
"description": "A charge that is not yet settled in full.",
|
|
159
|
+
"required": [
|
|
160
|
+
"entryId",
|
|
161
|
+
"origin",
|
|
162
|
+
"periodStart",
|
|
163
|
+
"periodEnd",
|
|
164
|
+
"currency",
|
|
165
|
+
"amountGross",
|
|
166
|
+
"settledGross",
|
|
167
|
+
"openGross",
|
|
168
|
+
"dueAt"
|
|
169
|
+
],
|
|
170
|
+
"additionalProperties": false,
|
|
171
|
+
"properties": {
|
|
172
|
+
"entryId": { "type": "string", "format": "uuid" },
|
|
173
|
+
"origin": { "$ref": "#/$defs/ChargeOrigin" },
|
|
174
|
+
"periodStart": { "type": "string", "format": "date-time" },
|
|
175
|
+
"periodEnd": { "type": "string", "format": "date-time" },
|
|
176
|
+
"currency": { "$ref": "#/$defs/Currency" },
|
|
177
|
+
"amountGross": { "type": "number" },
|
|
178
|
+
"settledGross": {
|
|
179
|
+
"type": "number",
|
|
180
|
+
"description": "What payments naming this charge have settled so far."
|
|
181
|
+
},
|
|
182
|
+
"openGross": {
|
|
183
|
+
"type": "number",
|
|
184
|
+
"description": "amountGross minus settledGross."
|
|
185
|
+
},
|
|
186
|
+
"dueAt": { "type": "string", "format": "date-time" }
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
|
|
190
|
+
"TenantAccount": {
|
|
191
|
+
"type": "object",
|
|
192
|
+
"title": "TenantAccount",
|
|
193
|
+
"description": "What a tenant and an administrator both read: the balance, what is open, and the history. One shape for both, because a balance a tenant is shown and a balance an administrator is shown must not be two answers.",
|
|
194
|
+
"required": [
|
|
195
|
+
"tenantId",
|
|
196
|
+
"currency",
|
|
197
|
+
"asOf",
|
|
198
|
+
"chargedGross",
|
|
199
|
+
"paidGross",
|
|
200
|
+
"balanceGross",
|
|
201
|
+
"openItems",
|
|
202
|
+
"entries"
|
|
203
|
+
],
|
|
204
|
+
"additionalProperties": false,
|
|
205
|
+
"properties": {
|
|
206
|
+
"tenantId": { "type": "string" },
|
|
207
|
+
"currency": { "$ref": "#/$defs/Currency" },
|
|
208
|
+
"asOf": {
|
|
209
|
+
"type": "string",
|
|
210
|
+
"format": "date-time",
|
|
211
|
+
"description": "A balance is only ever true at a moment, and this is the moment."
|
|
212
|
+
},
|
|
213
|
+
"chargedGross": { "type": "number" },
|
|
214
|
+
"paidGross": { "type": "number" },
|
|
215
|
+
"balanceGross": {
|
|
216
|
+
"type": "number",
|
|
217
|
+
"description": "chargedGross minus paidGross. Positive means the tenant owes; negative means they have paid ahead."
|
|
218
|
+
},
|
|
219
|
+
"openItems": {
|
|
220
|
+
"type": "array",
|
|
221
|
+
"items": { "$ref": "#/$defs/TenantAccountOpenItem" }
|
|
222
|
+
},
|
|
223
|
+
"entries": {
|
|
224
|
+
"type": "array",
|
|
225
|
+
"description": "The history the read was asked for, newest first. The window is the caller's decision, not a property of the account.",
|
|
226
|
+
"items": {
|
|
227
|
+
"oneOf": [
|
|
228
|
+
{ "$ref": "#/$defs/LedgerCharge" },
|
|
229
|
+
{ "$ref": "#/$defs/LedgerPayment" }
|
|
230
|
+
]
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|