@saasicat/spec 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +59 -0
  3. package/acceptance/README.md +62 -0
  4. package/acceptance/manifest/full-manifest-requires-super-admin.yaml +43 -0
  5. package/acceptance/manifest/public-boot-no-auth.yaml +40 -0
  6. package/acceptance/mfa/totp-verify-good-and-bad-code.yaml +57 -0
  7. package/acceptance/plan-version/publish-does-not-touch-bestand.yaml +62 -0
  8. package/acceptance/promo/first-time-only-blocks-second-redemption.yaml +40 -0
  9. package/acceptance/tenant/suspend-creates-audit-and-blocks-login.yaml +69 -0
  10. package/admin-api.openapi.yaml +1724 -0
  11. package/cli-conventions.md +158 -0
  12. package/index.cjs +18 -0
  13. package/index.d.cts +13 -0
  14. package/index.d.ts +14 -0
  15. package/index.js +17 -0
  16. package/package.json +63 -0
  17. package/prisma-fragments/01-subscription.prisma +216 -0
  18. package/prisma-fragments/02-promo-code.prisma +145 -0
  19. package/prisma-fragments/03-plan-versions.prisma +94 -0
  20. package/prisma-fragments/04-audit-log.prisma +38 -0
  21. package/prisma-fragments/05-bundle-business-type.prisma +206 -0
  22. package/prisma-fragments/06-catalog-entries.prisma +279 -0
  23. package/prisma-fragments/07-promotion.prisma +65 -0
  24. package/prisma-fragments/08-subscription-contract.prisma +92 -0
  25. package/prisma-fragments/09-pending-registration.prisma +96 -0
  26. package/prisma-fragments/README.md +115 -0
  27. package/schemas/admin-manifest.schema.json +328 -0
  28. package/schemas/audit-event.schema.json +73 -0
  29. package/schemas/plan-catalog.schema.json +166 -0
  30. package/schemas/promo-code.schema.json +214 -0
@@ -0,0 +1,73 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://saasicat.dev/schemas/audit-event.schema.json",
4
+ "title": "AuditEvent",
5
+ "description": "Language-neutral format of an AuditLog entry. Each consumer app has its own AuditLog table, same structure.",
6
+ "type": "object",
7
+ "required": ["id", "entity", "entityId", "action", "createdAt"],
8
+ "additionalProperties": false,
9
+ "properties": {
10
+ "id": { "type": "string", "format": "uuid" },
11
+ "tenantId": {
12
+ "type": ["string", "null"],
13
+ "description": "null = platform action (e.g. SUPER_ADMIN without tenant context)."
14
+ },
15
+ "userId": {
16
+ "type": ["string", "null"],
17
+ "description": "null = system / cron-triggered."
18
+ },
19
+ "userEmail": {
20
+ "type": ["string", "null"],
21
+ "format": "email",
22
+ "description": "Convenience lookup for display; backend resolves from userId."
23
+ },
24
+ "entity": {
25
+ "type": "string",
26
+ "description": "e.g. 'Tenant', 'PromoCode', 'Subscription', 'PlanVersion', 'User'."
27
+ },
28
+ "entityId": { "type": "string" },
29
+ "action": {
30
+ "type": "string",
31
+ "pattern": "^[A-Z][A-Z0-9_]+$",
32
+ "description": "SCREAMING_SNAKE_CASE, past-tense oriented. Examples: TENANT_SUSPEND, PILOT_GRANT, PROMO_CODE_CREATE, PLAN_VERSION_PUBLISH."
33
+ },
34
+ "changes": {
35
+ "type": ["object", "null"],
36
+ "description": "Freely structured JSON. Convention for field diffs: { field: { old, new } }. For workflow actions: { reason, actor_tag, ... }.",
37
+ "additionalProperties": true
38
+ },
39
+ "actorTag": {
40
+ "type": ["string", "null"],
41
+ "description": "Origin marker. Format: 'web:<email>:<sessionId>' or 'cli:<email>:<host>'."
42
+ },
43
+ "ipAddress": { "type": ["string", "null"] },
44
+ "userAgent": { "type": ["string", "null"] },
45
+ "createdAt": { "type": "string", "format": "date-time" }
46
+ },
47
+
48
+ "$defs": {
49
+ "ActorTagPattern": {
50
+ "type": "string",
51
+ "pattern": "^(web|cli):[^:]+:[^:]+$",
52
+ "description": "Mandatory format for actorTag."
53
+ },
54
+
55
+ "AuditQuery": {
56
+ "type": "object",
57
+ "description": "Query parameters for GET /api/v1/admin/audit. Template admin-api.openapi.yaml.",
58
+ "additionalProperties": false,
59
+ "properties": {
60
+ "tenantId": { "type": "string" },
61
+ "userId": { "type": "string" },
62
+ "entity": { "type": "string" },
63
+ "entityId": { "type": "string" },
64
+ "action": { "type": "string", "pattern": "^[A-Z][A-Z0-9_]+$" },
65
+ "actorTag": { "type": "string", "description": "Wildcard-capable, e.g. 'cli:*'." },
66
+ "from": { "type": "string", "format": "date-time" },
67
+ "to": { "type": "string", "format": "date-time" },
68
+ "page": { "type": "integer", "minimum": 1, "default": 1 },
69
+ "pageSize": { "type": "integer", "minimum": 1, "maximum": 200, "default": 50 }
70
+ }
71
+ }
72
+ }
73
+ }
@@ -0,0 +1,166 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://saasicat.dev/schemas/plan-catalog.schema.json",
4
+ "title": "PlanCatalog",
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
+ "type": "object",
7
+ "required": ["schemaVersion", "projectKey", "currency", "vatRate"],
8
+ "additionalProperties": false,
9
+ "properties": {
10
+ "schemaVersion": {
11
+ "type": "integer",
12
+ "const": 1
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
+ "app": {
20
+ "type": "object",
21
+ "additionalProperties": false,
22
+ "description": "App identity block (branding + version). Consumed by AdminPublicBootController + AdminManifestConfigFactory.",
23
+ "required": ["name"],
24
+ "properties": {
25
+ "name": {
26
+ "type": "string",
27
+ "minLength": 1,
28
+ "description": "Brand display name (e.g. \"My App\")."
29
+ },
30
+ "label": {
31
+ "type": "string",
32
+ "description": "Tag/subtitle in the brand block (e.g. \"SuperAdmin\")."
33
+ },
34
+ "version": {
35
+ "type": "string",
36
+ "description": "App version string (build info)."
37
+ },
38
+ "icon": {
39
+ "type": "string",
40
+ "minLength": 1,
41
+ "maxLength": 8,
42
+ "description": "Short abbreviation for the logo badge (e.g. \"ma\", \"da\")."
43
+ },
44
+ "logoUrl": {
45
+ "type": "string",
46
+ "format": "uri-reference",
47
+ "description": "Optional URL to PNG/SVG; when set, the UI renders an <img> instead of the initials badge."
48
+ }
49
+ }
50
+ },
51
+ "currency": {
52
+ "type": "string",
53
+ "pattern": "^[A-Z]{3}$",
54
+ "description": "ISO 4217 currency code. EUR-only in phase 1."
55
+ },
56
+ "vatRate": {
57
+ "type": "number",
58
+ "minimum": 0,
59
+ "maximum": 100,
60
+ "description": "VAT rate in percent. Required (even when 0)."
61
+ },
62
+ "marketing": {
63
+ "type": "object",
64
+ "additionalProperties": false,
65
+ "description": "App-wide marketing configuration. SPEC_V2 §6.5.",
66
+ "required": ["availableLocales"],
67
+ "properties": {
68
+ "availableLocales": {
69
+ "type": "array",
70
+ "items": { "type": "string", "pattern": "^[a-z]{2}(-[A-Z]{2})?$" },
71
+ "minItems": 1,
72
+ "uniqueItems": true,
73
+ "description": "Allowed language pool the app may market. First = default locale. From it the SuperAdmin activates a subset in the marketing catalog (LocaleManager)."
74
+ }
75
+ }
76
+ },
77
+ "features": {
78
+ "type": "array",
79
+ "items": { "$ref": "#/$defs/FeatureDef" },
80
+ "description": "Master list of all feature flags of the project. Plans may only reference keys declared here."
81
+ },
82
+ "plans": {
83
+ "type": "array",
84
+ "items": { "$ref": "#/$defs/PlanDef" },
85
+ "description": "Optional. When omitted, plans come exclusively from the AdminUI / DB."
86
+ }
87
+ },
88
+
89
+ "$defs": {
90
+ "FeatureDef": {
91
+ "type": "object",
92
+ "required": ["key"],
93
+ "additionalProperties": false,
94
+ "properties": {
95
+ "key": {
96
+ "type": "string",
97
+ "pattern": "^[A-Z][A-Z0-9_]+$",
98
+ "description": "SCREAMING_SNAKE_CASE; unique per catalog."
99
+ },
100
+ "label": { "type": "string", "minLength": 1 },
101
+ "icon": { "type": "string" },
102
+ "tier": {
103
+ "type": "string",
104
+ "description": "Optional logical group. Convention: CORE / ADVANCED / PRO / BUSINESS / ENTERPRISE_ONLY."
105
+ },
106
+ "plannedOnly": {
107
+ "type": "boolean",
108
+ "default": false,
109
+ "description": "Marks that the feature is not in production (no plan or bundle references it)."
110
+ }
111
+ }
112
+ },
113
+
114
+ "PlanDef": {
115
+ "type": "object",
116
+ "required": ["id", "quotas", "features"],
117
+ "additionalProperties": false,
118
+ "properties": {
119
+ "id": {
120
+ "type": "string",
121
+ "pattern": "^[A-Z][A-Z0-9_]+$",
122
+ "description": "Plan ID. Freely chosen by the consumer (BASIC, STANDARD, ...). Ends up as a string in Subscription.plan."
123
+ },
124
+ "name": { "type": "string", "minLength": 1 },
125
+ "tagline": { "type": "string" },
126
+ "marketed": {
127
+ "type": "boolean",
128
+ "default": true,
129
+ "description": "false = not selectable in self-service onboarding (e.g. ENTERPRISE)."
130
+ },
131
+ "popular": {
132
+ "type": "boolean",
133
+ "default": false,
134
+ "description": "Highlighted card in onboarding (max. 1 per catalog)."
135
+ },
136
+ "monthlyNet": {
137
+ "type": ["number", "null"],
138
+ "minimum": 0,
139
+ "description": "Net monthly price. null = on request (sales contact)."
140
+ },
141
+ "yearlyNet": {
142
+ "type": ["number", "null"],
143
+ "minimum": 0,
144
+ "description": "Net total amount per year for BillingCycle=YEARLY (not the effective monthly price). null = monthly only."
145
+ },
146
+ "quotas": {
147
+ "type": "object",
148
+ "patternProperties": {
149
+ "^[a-z][A-Za-z0-9]*$": {
150
+ "type": "integer",
151
+ "description": "-1 = unlimited; 0 = not allowed; >0 = hard upper limit."
152
+ }
153
+ },
154
+ "additionalProperties": false,
155
+ "minProperties": 1
156
+ },
157
+ "features": {
158
+ "type": "array",
159
+ "items": { "type": "string", "pattern": "^[A-Z][A-Z0-9_]+$" },
160
+ "uniqueItems": true,
161
+ "description": "List of included feature keys; all must exist in features[].key."
162
+ }
163
+ }
164
+ }
165
+ }
166
+ }
@@ -0,0 +1,214 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://saasicat.dev/schemas/promo-code.schema.json",
4
+ "title": "PromoCode",
5
+ "description": "Language-neutral definition of promo codes for the SaaS platform, including redemption lifecycle.",
6
+ "type": "object",
7
+ "$defs": {
8
+ "ValueType": {
9
+ "type": "string",
10
+ "enum": ["PERCENT", "ABSOLUTE"],
11
+ "description": "PERCENT: value is a percentage (1–100). ABSOLUTE: value is a net amount in the catalog currency."
12
+ },
13
+
14
+ "DurationType": {
15
+ "type": "string",
16
+ "enum": ["ONCE", "MONTHS", "BILLING_CYCLES"],
17
+ "description": "ONCE: one-time discount. MONTHS: durationValue months from redemption. BILLING_CYCLES: durationValue billing cycles."
18
+ },
19
+
20
+ "Status": {
21
+ "type": "string",
22
+ "enum": ["ACTIVE", "PAUSED", "EXHAUSTED", "EXPIRED"]
23
+ },
24
+
25
+ "BillingCycle": {
26
+ "type": "string",
27
+ "enum": ["MONTHLY", "YEARLY"]
28
+ },
29
+
30
+ "RedemptionStatus": {
31
+ "type": "string",
32
+ "enum": ["ACTIVE", "REVERSED", "EXPIRED"]
33
+ },
34
+
35
+ "ValidationResult": {
36
+ "type": "string",
37
+ "enum": [
38
+ "VALID",
39
+ "EXPIRED",
40
+ "EXHAUSTED",
41
+ "NOT_FOUND",
42
+ "PAUSED",
43
+ "NOT_APPLICABLE",
44
+ "FIRST_TIME_ONLY",
45
+ "ZERO_INVOICE_BLOCKED",
46
+ "MIN_AMOUNT_NOT_REACHED",
47
+ "RATE_LIMITED"
48
+ ]
49
+ },
50
+
51
+ "CreatePromoCodeRequest": {
52
+ "type": "object",
53
+ "required": ["code", "valueType", "value", "durationType"],
54
+ "additionalProperties": false,
55
+ "properties": {
56
+ "code": {
57
+ "type": "string",
58
+ "pattern": "^[A-Z0-9_-]{4,32}$",
59
+ "description": "Uppercase letters, digits, '-' and '_'. Checked case-insensitively, stored in UPPER."
60
+ },
61
+ "valueType": { "$ref": "#/$defs/ValueType" },
62
+ "value": {
63
+ "type": "number",
64
+ "exclusiveMinimum": 0,
65
+ "multipleOf": 0.01,
66
+ "description": "For PERCENT: 0.01–100. For ABSOLUTE: > 0 in catalog currency."
67
+ },
68
+ "durationType": { "$ref": "#/$defs/DurationType" },
69
+ "durationValue": {
70
+ "type": ["integer", "null"],
71
+ "minimum": 1,
72
+ "maximum": 24,
73
+ "description": "Required for MONTHS / BILLING_CYCLES, null for ONCE."
74
+ },
75
+ "validFrom": { "type": ["string", "null"], "format": "date-time" },
76
+ "validUntil": { "type": ["string", "null"], "format": "date-time" },
77
+ "maxRedemptions": { "type": ["integer", "null"], "minimum": 1 },
78
+ "appliesToPlans": {
79
+ "type": "array",
80
+ "items": { "type": "string", "pattern": "^[A-Z][A-Z0-9_]+$" },
81
+ "uniqueItems": true,
82
+ "description": "Empty = all plans."
83
+ },
84
+ "appliesToBilling": {
85
+ "anyOf": [{ "$ref": "#/$defs/BillingCycle" }, { "type": "null" }]
86
+ },
87
+ "firstTimeCustomersOnly": { "type": "boolean", "default": true },
88
+ "minimumPlanAmountGross": {
89
+ "type": ["number", "null"],
90
+ "exclusiveMinimum": 0,
91
+ "multipleOf": 0.01
92
+ },
93
+ "allowZeroInvoice": {
94
+ "type": "boolean",
95
+ "default": false,
96
+ "description": "Default: discount must not reduce the invoice to 0."
97
+ },
98
+ "description": { "type": ["string", "null"], "maxLength": 1000 },
99
+ "campaignTag": { "type": ["string", "null"], "maxLength": 100 },
100
+ "revenueDeductionAccount": {
101
+ "type": ["string", "null"],
102
+ "maxLength": 16,
103
+ "description": "SKR account for accounting; project-specific."
104
+ }
105
+ }
106
+ },
107
+
108
+ "UpdatePromoCodeRequest": {
109
+ "type": "object",
110
+ "additionalProperties": false,
111
+ "properties": {
112
+ "status": {
113
+ "type": "string",
114
+ "enum": ["ACTIVE", "PAUSED"]
115
+ },
116
+ "description": { "type": ["string", "null"], "maxLength": 1000 },
117
+ "validUntil": { "type": ["string", "null"], "format": "date-time" },
118
+ "maxRedemptions": { "type": ["integer", "null"], "minimum": 1 },
119
+ "campaignTag": { "type": ["string", "null"], "maxLength": 100 }
120
+ }
121
+ },
122
+
123
+ "PromoCode": {
124
+ "type": "object",
125
+ "required": [
126
+ "id",
127
+ "code",
128
+ "valueType",
129
+ "value",
130
+ "durationType",
131
+ "status",
132
+ "redemptionsCount"
133
+ ],
134
+ "additionalProperties": false,
135
+ "properties": {
136
+ "id": { "type": "string", "format": "uuid" },
137
+ "code": { "type": "string", "pattern": "^[A-Z0-9_-]{4,32}$" },
138
+ "valueType": { "$ref": "#/$defs/ValueType" },
139
+ "value": { "type": "number", "exclusiveMinimum": 0 },
140
+ "durationType": { "$ref": "#/$defs/DurationType" },
141
+ "durationValue": { "type": ["integer", "null"], "minimum": 1, "maximum": 24 },
142
+ "validFrom": { "type": ["string", "null"], "format": "date-time" },
143
+ "validUntil": { "type": ["string", "null"], "format": "date-time" },
144
+ "maxRedemptions": { "type": ["integer", "null"], "minimum": 1 },
145
+ "redemptionsCount": { "type": "integer", "minimum": 0 },
146
+ "appliesToPlans": {
147
+ "type": "array",
148
+ "items": { "type": "string", "pattern": "^[A-Z][A-Z0-9_]+$" }
149
+ },
150
+ "appliesToBilling": {
151
+ "anyOf": [{ "$ref": "#/$defs/BillingCycle" }, { "type": "null" }]
152
+ },
153
+ "firstTimeCustomersOnly": { "type": "boolean" },
154
+ "minimumPlanAmountGross": { "type": ["number", "null"] },
155
+ "allowZeroInvoice": { "type": "boolean" },
156
+ "status": { "$ref": "#/$defs/Status" },
157
+ "description": { "type": ["string", "null"] },
158
+ "campaignTag": { "type": ["string", "null"] },
159
+ "revenueDeductionAccount": { "type": ["string", "null"] },
160
+ "createdAt": { "type": "string", "format": "date-time" },
161
+ "deletedAt": { "type": ["string", "null"], "format": "date-time" }
162
+ }
163
+ },
164
+
165
+ "PromoCodeRedemption": {
166
+ "type": "object",
167
+ "required": [
168
+ "id",
169
+ "promoCodeId",
170
+ "subscriptionId",
171
+ "tenantId",
172
+ "appliedValueType",
173
+ "appliedValue",
174
+ "status",
175
+ "redeemedAt"
176
+ ],
177
+ "additionalProperties": false,
178
+ "properties": {
179
+ "id": { "type": "string", "format": "uuid" },
180
+ "promoCodeId": { "type": "string", "format": "uuid" },
181
+ "subscriptionId": { "type": "string", "format": "uuid" },
182
+ "tenantId": { "type": "string" },
183
+ "appliedValueType": { "$ref": "#/$defs/ValueType" },
184
+ "appliedValue": { "type": "number" },
185
+ "appliedDurationType": { "$ref": "#/$defs/DurationType" },
186
+ "appliedDurationValue": { "type": ["integer", "null"] },
187
+ "startsAt": { "type": "string", "format": "date-time" },
188
+ "endsAt": { "type": ["string", "null"], "format": "date-time" },
189
+ "status": { "$ref": "#/$defs/RedemptionStatus" },
190
+ "redeemedAt": { "type": "string", "format": "date-time" },
191
+ "reversedAt": { "type": ["string", "null"], "format": "date-time" }
192
+ }
193
+ },
194
+
195
+ "PromoCodeValidationLog": {
196
+ "type": "object",
197
+ "required": ["id", "codeAttempt", "result", "createdAt"],
198
+ "additionalProperties": false,
199
+ "properties": {
200
+ "id": { "type": "string", "format": "uuid" },
201
+ "promoCodeId": {
202
+ "type": ["string", "null"],
203
+ "format": "uuid",
204
+ "description": "null for NOT_FOUND"
205
+ },
206
+ "codeAttempt": { "type": "string" },
207
+ "ipHash": { "type": ["string", "null"], "description": "no plaintext IP" },
208
+ "sessionId": { "type": ["string", "null"] },
209
+ "result": { "$ref": "#/$defs/ValidationResult" },
210
+ "createdAt": { "type": "string", "format": "date-time" }
211
+ }
212
+ }
213
+ }
214
+ }