@contractspec/example.saas-boilerplate 0.0.0-canary-20260113170453
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/.turbo/turbo-build$colon$bundle.log +188 -0
- package/.turbo/turbo-build.log +189 -0
- package/CHANGELOG.md +440 -0
- package/LICENSE +21 -0
- package/README.md +155 -0
- package/dist/billing/billing.entity.d.ts +61 -0
- package/dist/billing/billing.entity.d.ts.map +1 -0
- package/dist/billing/billing.entity.js +122 -0
- package/dist/billing/billing.entity.js.map +1 -0
- package/dist/billing/billing.enum.d.ts +16 -0
- package/dist/billing/billing.enum.d.ts.map +1 -0
- package/dist/billing/billing.enum.js +27 -0
- package/dist/billing/billing.enum.js.map +1 -0
- package/dist/billing/billing.event.d.ts +86 -0
- package/dist/billing/billing.event.d.ts.map +1 -0
- package/dist/billing/billing.event.js +153 -0
- package/dist/billing/billing.event.js.map +1 -0
- package/dist/billing/billing.handler.d.ts +82 -0
- package/dist/billing/billing.handler.d.ts.map +1 -0
- package/dist/billing/billing.handler.js +58 -0
- package/dist/billing/billing.handler.js.map +1 -0
- package/dist/billing/billing.operations.d.ts +166 -0
- package/dist/billing/billing.operations.d.ts.map +1 -0
- package/dist/billing/billing.operations.js +181 -0
- package/dist/billing/billing.operations.js.map +1 -0
- package/dist/billing/billing.presentation.d.ts +14 -0
- package/dist/billing/billing.presentation.d.ts.map +1 -0
- package/dist/billing/billing.presentation.js +59 -0
- package/dist/billing/billing.presentation.js.map +1 -0
- package/dist/billing/billing.schema.d.ts +201 -0
- package/dist/billing/billing.schema.d.ts.map +1 -0
- package/dist/billing/billing.schema.js +214 -0
- package/dist/billing/billing.schema.js.map +1 -0
- package/dist/billing/index.d.ts +8 -0
- package/dist/billing/index.js +9 -0
- package/dist/dashboard/dashboard.presentation.d.ts +14 -0
- package/dist/dashboard/dashboard.presentation.d.ts.map +1 -0
- package/dist/dashboard/dashboard.presentation.js +55 -0
- package/dist/dashboard/dashboard.presentation.js.map +1 -0
- package/dist/dashboard/index.d.ts +2 -0
- package/dist/dashboard/index.js +3 -0
- package/dist/docs/index.d.ts +1 -0
- package/dist/docs/index.js +1 -0
- package/dist/docs/saas-boilerplate.docblock.d.ts +1 -0
- package/dist/docs/saas-boilerplate.docblock.js +100 -0
- package/dist/docs/saas-boilerplate.docblock.js.map +1 -0
- package/dist/example.d.ts +7 -0
- package/dist/example.d.ts.map +1 -0
- package/dist/example.js +53 -0
- package/dist/example.js.map +1 -0
- package/dist/handlers/index.d.ts +4 -0
- package/dist/handlers/index.js +5 -0
- package/dist/handlers/saas.handlers.d.ts +68 -0
- package/dist/handlers/saas.handlers.d.ts.map +1 -0
- package/dist/handlers/saas.handlers.js +148 -0
- package/dist/handlers/saas.handlers.js.map +1 -0
- package/dist/index.d.ts +54 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +81 -0
- package/dist/index.js.map +1 -0
- package/dist/presentations/index.d.ts +17 -0
- package/dist/presentations/index.d.ts.map +1 -0
- package/dist/presentations/index.js +17 -0
- package/dist/presentations/index.js.map +1 -0
- package/dist/project/index.d.ts +8 -0
- package/dist/project/index.js +9 -0
- package/dist/project/project.entity.d.ts +40 -0
- package/dist/project/project.entity.d.ts.map +1 -0
- package/dist/project/project.entity.js +85 -0
- package/dist/project/project.entity.js.map +1 -0
- package/dist/project/project.enum.d.ts +16 -0
- package/dist/project/project.enum.d.ts.map +1 -0
- package/dist/project/project.enum.js +26 -0
- package/dist/project/project.enum.js.map +1 -0
- package/dist/project/project.event.d.ts +92 -0
- package/dist/project/project.event.d.ts.map +1 -0
- package/dist/project/project.event.js +165 -0
- package/dist/project/project.event.js.map +1 -0
- package/dist/project/project.handler.d.ts +72 -0
- package/dist/project/project.handler.d.ts.map +1 -0
- package/dist/project/project.handler.js +82 -0
- package/dist/project/project.handler.js.map +1 -0
- package/dist/project/project.operations.d.ts +419 -0
- package/dist/project/project.operations.d.ts.map +1 -0
- package/dist/project/project.operations.js +260 -0
- package/dist/project/project.operations.js.map +1 -0
- package/dist/project/project.presentation.d.ts +14 -0
- package/dist/project/project.presentation.d.ts.map +1 -0
- package/dist/project/project.presentation.js +65 -0
- package/dist/project/project.presentation.js.map +1 -0
- package/dist/project/project.schema.d.ts +235 -0
- package/dist/project/project.schema.d.ts.map +1 -0
- package/dist/project/project.schema.js +215 -0
- package/dist/project/project.schema.js.map +1 -0
- package/dist/saas-boilerplate.feature.d.ts +12 -0
- package/dist/saas-boilerplate.feature.d.ts.map +1 -0
- package/dist/saas-boilerplate.feature.js +208 -0
- package/dist/saas-boilerplate.feature.js.map +1 -0
- package/dist/seeders/index.d.ts +10 -0
- package/dist/seeders/index.d.ts.map +1 -0
- package/dist/seeders/index.js +19 -0
- package/dist/seeders/index.js.map +1 -0
- package/dist/settings/index.d.ts +3 -0
- package/dist/settings/index.js +4 -0
- package/dist/settings/settings.entity.d.ts +37 -0
- package/dist/settings/settings.entity.d.ts.map +1 -0
- package/dist/settings/settings.entity.js +78 -0
- package/dist/settings/settings.entity.js.map +1 -0
- package/dist/settings/settings.enum.d.ts +10 -0
- package/dist/settings/settings.enum.d.ts.map +1 -0
- package/dist/settings/settings.enum.js +21 -0
- package/dist/settings/settings.enum.js.map +1 -0
- package/dist/shared/mock-data.d.ts +86 -0
- package/dist/shared/mock-data.d.ts.map +1 -0
- package/dist/shared/mock-data.js +138 -0
- package/dist/shared/mock-data.js.map +1 -0
- package/dist/shared/overlay-types.d.ts +34 -0
- package/dist/shared/overlay-types.d.ts.map +1 -0
- package/dist/shared/overlay-types.js +0 -0
- package/dist/tests/operations.test-spec.d.ts +10 -0
- package/dist/tests/operations.test-spec.d.ts.map +1 -0
- package/dist/tests/operations.test-spec.js +123 -0
- package/dist/tests/operations.test-spec.js.map +1 -0
- package/dist/ui/SaasDashboard.d.ts +7 -0
- package/dist/ui/SaasDashboard.d.ts.map +1 -0
- package/dist/ui/SaasDashboard.js +298 -0
- package/dist/ui/SaasDashboard.js.map +1 -0
- package/dist/ui/SaasProjectList.d.ts +14 -0
- package/dist/ui/SaasProjectList.d.ts.map +1 -0
- package/dist/ui/SaasProjectList.js +76 -0
- package/dist/ui/SaasProjectList.js.map +1 -0
- package/dist/ui/SaasSettingsPanel.d.ts +7 -0
- package/dist/ui/SaasSettingsPanel.d.ts.map +1 -0
- package/dist/ui/SaasSettingsPanel.js +138 -0
- package/dist/ui/SaasSettingsPanel.js.map +1 -0
- package/dist/ui/hooks/index.d.ts +3 -0
- package/dist/ui/hooks/index.js +6 -0
- package/dist/ui/hooks/useProjectList.d.ts +34 -0
- package/dist/ui/hooks/useProjectList.d.ts.map +1 -0
- package/dist/ui/hooks/useProjectList.js +75 -0
- package/dist/ui/hooks/useProjectList.js.map +1 -0
- package/dist/ui/hooks/useProjectMutations.d.ts +28 -0
- package/dist/ui/hooks/useProjectMutations.d.ts.map +1 -0
- package/dist/ui/hooks/useProjectMutations.js +146 -0
- package/dist/ui/hooks/useProjectMutations.js.map +1 -0
- package/dist/ui/index.d.ts +14 -0
- package/dist/ui/index.js +15 -0
- package/dist/ui/modals/CreateProjectModal.d.ts +23 -0
- package/dist/ui/modals/CreateProjectModal.d.ts.map +1 -0
- package/dist/ui/modals/CreateProjectModal.js +139 -0
- package/dist/ui/modals/CreateProjectModal.js.map +1 -0
- package/dist/ui/modals/ProjectActionsModal.d.ts +38 -0
- package/dist/ui/modals/ProjectActionsModal.d.ts.map +1 -0
- package/dist/ui/modals/ProjectActionsModal.js +292 -0
- package/dist/ui/modals/ProjectActionsModal.js.map +1 -0
- package/dist/ui/modals/index.d.ts +3 -0
- package/dist/ui/modals/index.js +4 -0
- package/dist/ui/overlays/demo-overlays.d.ts +19 -0
- package/dist/ui/overlays/demo-overlays.d.ts.map +1 -0
- package/dist/ui/overlays/demo-overlays.js +70 -0
- package/dist/ui/overlays/demo-overlays.js.map +1 -0
- package/dist/ui/overlays/index.d.ts +2 -0
- package/dist/ui/overlays/index.js +3 -0
- package/dist/ui/renderers/index.d.ts +3 -0
- package/dist/ui/renderers/index.js +4 -0
- package/dist/ui/renderers/project-list.markdown.d.ts +31 -0
- package/dist/ui/renderers/project-list.markdown.d.ts.map +1 -0
- package/dist/ui/renderers/project-list.markdown.js +148 -0
- package/dist/ui/renderers/project-list.markdown.js.map +1 -0
- package/dist/ui/renderers/project-list.renderer.d.ts +9 -0
- package/dist/ui/renderers/project-list.renderer.d.ts.map +1 -0
- package/dist/ui/renderers/project-list.renderer.js +17 -0
- package/dist/ui/renderers/project-list.renderer.js.map +1 -0
- package/example.ts +1 -0
- package/package.json +135 -0
- package/src/billing/billing.entity.ts +158 -0
- package/src/billing/billing.enum.ts +23 -0
- package/src/billing/billing.event.ts +108 -0
- package/src/billing/billing.handler.ts +137 -0
- package/src/billing/billing.operations.ts +187 -0
- package/src/billing/billing.presentation.ts +56 -0
- package/src/billing/billing.schema.ts +133 -0
- package/src/billing/index.ts +64 -0
- package/src/dashboard/dashboard.presentation.ts +56 -0
- package/src/dashboard/index.ts +8 -0
- package/src/docs/index.ts +1 -0
- package/src/docs/saas-boilerplate.docblock.ts +98 -0
- package/src/example.ts +38 -0
- package/src/handlers/index.ts +23 -0
- package/src/handlers/saas.handlers.ts +300 -0
- package/src/index.ts +76 -0
- package/src/presentations/index.ts +36 -0
- package/src/project/index.ts +66 -0
- package/src/project/project.entity.ts +93 -0
- package/src/project/project.enum.ts +22 -0
- package/src/project/project.event.ts +128 -0
- package/src/project/project.handler.ts +168 -0
- package/src/project/project.operations.ts +272 -0
- package/src/project/project.presentation.ts +58 -0
- package/src/project/project.schema.ts +147 -0
- package/src/saas-boilerplate.feature.ts +113 -0
- package/src/seeders/index.ts +28 -0
- package/src/settings/index.ts +9 -0
- package/src/settings/settings.entity.ts +89 -0
- package/src/settings/settings.enum.ts +11 -0
- package/src/shared/mock-data.ts +110 -0
- package/src/shared/overlay-types.ts +39 -0
- package/src/tests/operations.test-spec.ts +109 -0
- package/src/ui/SaasDashboard.tsx +325 -0
- package/src/ui/SaasProjectList.tsx +113 -0
- package/src/ui/SaasSettingsPanel.tsx +96 -0
- package/src/ui/hooks/index.ts +10 -0
- package/src/ui/hooks/useProjectList.ts +95 -0
- package/src/ui/hooks/useProjectMutations.ts +166 -0
- package/src/ui/index.ts +18 -0
- package/src/ui/modals/CreateProjectModal.tsx +176 -0
- package/src/ui/modals/ProjectActionsModal.tsx +346 -0
- package/src/ui/modals/index.ts +2 -0
- package/src/ui/overlays/demo-overlays.ts +74 -0
- package/src/ui/overlays/index.ts +1 -0
- package/src/ui/renderers/index.ts +7 -0
- package/src/ui/renderers/project-list.markdown.ts +239 -0
- package/src/ui/renderers/project-list.renderer.tsx +22 -0
- package/tsconfig.json +10 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/tsdown.config.js +7 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { ScalarTypeEnum, defineSchemaModel } from '@contractspec/lib.schema';
|
|
2
|
+
import { defineEvent } from '@contractspec/lib.contracts';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Payload when feature usage is recorded.
|
|
6
|
+
*/
|
|
7
|
+
const UsageRecordedPayload = defineSchemaModel({
|
|
8
|
+
name: 'UsageRecordedPayload',
|
|
9
|
+
description: 'Payload when feature usage is recorded',
|
|
10
|
+
fields: {
|
|
11
|
+
organizationId: {
|
|
12
|
+
type: ScalarTypeEnum.String_unsecure(),
|
|
13
|
+
isOptional: false,
|
|
14
|
+
},
|
|
15
|
+
feature: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
|
|
16
|
+
quantity: { type: ScalarTypeEnum.Int_unsecure(), isOptional: false },
|
|
17
|
+
billingPeriod: {
|
|
18
|
+
type: ScalarTypeEnum.String_unsecure(),
|
|
19
|
+
isOptional: false,
|
|
20
|
+
},
|
|
21
|
+
recordedAt: { type: ScalarTypeEnum.DateTime(), isOptional: false },
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Payload when usage limit is reached.
|
|
27
|
+
*/
|
|
28
|
+
const UsageLimitReachedPayload = defineSchemaModel({
|
|
29
|
+
name: 'UsageLimitReachedPayload',
|
|
30
|
+
description: 'Payload when usage limit is reached',
|
|
31
|
+
fields: {
|
|
32
|
+
organizationId: {
|
|
33
|
+
type: ScalarTypeEnum.String_unsecure(),
|
|
34
|
+
isOptional: false,
|
|
35
|
+
},
|
|
36
|
+
feature: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
|
|
37
|
+
limit: { type: ScalarTypeEnum.Int_unsecure(), isOptional: false },
|
|
38
|
+
currentUsage: { type: ScalarTypeEnum.Int_unsecure(), isOptional: false },
|
|
39
|
+
reachedAt: { type: ScalarTypeEnum.DateTime(), isOptional: false },
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Payload when subscription status changes.
|
|
45
|
+
*/
|
|
46
|
+
const SubscriptionChangedPayload = defineSchemaModel({
|
|
47
|
+
name: 'SubscriptionChangedPayload',
|
|
48
|
+
description: 'Payload when subscription status changes',
|
|
49
|
+
fields: {
|
|
50
|
+
organizationId: {
|
|
51
|
+
type: ScalarTypeEnum.String_unsecure(),
|
|
52
|
+
isOptional: false,
|
|
53
|
+
},
|
|
54
|
+
previousPlan: { type: ScalarTypeEnum.String_unsecure(), isOptional: true },
|
|
55
|
+
newPlan: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
|
|
56
|
+
previousStatus: {
|
|
57
|
+
type: ScalarTypeEnum.String_unsecure(),
|
|
58
|
+
isOptional: true,
|
|
59
|
+
},
|
|
60
|
+
newStatus: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
|
|
61
|
+
changedAt: { type: ScalarTypeEnum.DateTime(), isOptional: false },
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Event: Feature usage has been recorded.
|
|
67
|
+
*/
|
|
68
|
+
export const UsageRecordedEvent = defineEvent({
|
|
69
|
+
meta: {
|
|
70
|
+
key: 'billing.usage.recorded',
|
|
71
|
+
version: '1.0.0',
|
|
72
|
+
description: 'Feature usage has been recorded.',
|
|
73
|
+
stability: 'stable',
|
|
74
|
+
owners: ['@saas-team'],
|
|
75
|
+
tags: ['billing', 'usage', 'recorded'],
|
|
76
|
+
},
|
|
77
|
+
payload: UsageRecordedPayload,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Event: Usage limit has been reached for a feature.
|
|
82
|
+
*/
|
|
83
|
+
export const UsageLimitReachedEvent = defineEvent({
|
|
84
|
+
meta: {
|
|
85
|
+
key: 'billing.limit.reached',
|
|
86
|
+
version: '1.0.0',
|
|
87
|
+
description: 'Usage limit has been reached for a feature.',
|
|
88
|
+
stability: 'stable',
|
|
89
|
+
owners: ['@saas-team'],
|
|
90
|
+
tags: ['billing', 'limit', 'reached'],
|
|
91
|
+
},
|
|
92
|
+
payload: UsageLimitReachedPayload,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Event: Subscription status has changed.
|
|
97
|
+
*/
|
|
98
|
+
export const SubscriptionChangedEvent = defineEvent({
|
|
99
|
+
meta: {
|
|
100
|
+
key: 'billing.subscription.changed',
|
|
101
|
+
version: '1.0.0',
|
|
102
|
+
description: 'Subscription status has changed.',
|
|
103
|
+
stability: 'stable',
|
|
104
|
+
owners: ['@saas-team'],
|
|
105
|
+
tags: ['billing', 'subscription', 'changed'],
|
|
106
|
+
},
|
|
107
|
+
payload: SubscriptionChangedPayload,
|
|
108
|
+
});
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mock handlers for Billing contracts.
|
|
3
|
+
*/
|
|
4
|
+
import { MOCK_SUBSCRIPTION, MOCK_USAGE_SUMMARY } from '../shared/mock-data';
|
|
5
|
+
|
|
6
|
+
// Types inferred from contract schemas
|
|
7
|
+
export interface Subscription {
|
|
8
|
+
id: string;
|
|
9
|
+
organizationId: string;
|
|
10
|
+
planId: string;
|
|
11
|
+
planName: string;
|
|
12
|
+
status: 'ACTIVE' | 'TRIALING' | 'PAST_DUE' | 'CANCELED' | 'UNPAID';
|
|
13
|
+
currentPeriodStart: Date;
|
|
14
|
+
currentPeriodEnd: Date;
|
|
15
|
+
limits: {
|
|
16
|
+
projects: number;
|
|
17
|
+
users: number;
|
|
18
|
+
storage: number;
|
|
19
|
+
apiCalls: number;
|
|
20
|
+
};
|
|
21
|
+
usage: {
|
|
22
|
+
projects: number;
|
|
23
|
+
users: number;
|
|
24
|
+
storage: number;
|
|
25
|
+
apiCalls: number;
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface UsageSummary {
|
|
30
|
+
organizationId: string;
|
|
31
|
+
period: string;
|
|
32
|
+
apiCalls: {
|
|
33
|
+
total: number;
|
|
34
|
+
limit: number;
|
|
35
|
+
percentUsed: number;
|
|
36
|
+
};
|
|
37
|
+
storage: {
|
|
38
|
+
totalGb: number;
|
|
39
|
+
limitGb: number;
|
|
40
|
+
percentUsed: number;
|
|
41
|
+
};
|
|
42
|
+
activeProjects: number;
|
|
43
|
+
activeUsers: number;
|
|
44
|
+
breakdown: {
|
|
45
|
+
date: string;
|
|
46
|
+
apiCalls: number;
|
|
47
|
+
storageGb: number;
|
|
48
|
+
}[];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface RecordUsageInput {
|
|
52
|
+
metric: string;
|
|
53
|
+
quantity: number;
|
|
54
|
+
timestamp?: Date;
|
|
55
|
+
metadata?: Record<string, unknown>;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface CheckFeatureAccessInput {
|
|
59
|
+
feature: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface CheckFeatureAccessOutput {
|
|
63
|
+
allowed: boolean;
|
|
64
|
+
reason?:
|
|
65
|
+
| 'PLAN_LIMIT'
|
|
66
|
+
| 'FEATURE_NOT_INCLUDED'
|
|
67
|
+
| 'QUOTA_EXCEEDED'
|
|
68
|
+
| 'SUBSCRIPTION_INACTIVE';
|
|
69
|
+
currentUsage?: number;
|
|
70
|
+
limit?: number;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Mock handler for GetSubscriptionContract.
|
|
75
|
+
*/
|
|
76
|
+
export async function mockGetSubscriptionHandler(): Promise<Subscription> {
|
|
77
|
+
return MOCK_SUBSCRIPTION;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Mock handler for GetUsageSummaryContract.
|
|
82
|
+
*/
|
|
83
|
+
export async function mockGetUsageSummaryHandler(input: {
|
|
84
|
+
period?: string;
|
|
85
|
+
}): Promise<UsageSummary> {
|
|
86
|
+
return {
|
|
87
|
+
...MOCK_USAGE_SUMMARY,
|
|
88
|
+
period: input.period ?? 'current_month',
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Mock handler for RecordUsageContract.
|
|
94
|
+
*/
|
|
95
|
+
export async function mockRecordUsageHandler(
|
|
96
|
+
input: RecordUsageInput
|
|
97
|
+
): Promise<{ recorded: boolean; newTotal: number }> {
|
|
98
|
+
const currentUsage = MOCK_USAGE_SUMMARY.apiCalls.total;
|
|
99
|
+
const newTotal = currentUsage + input.quantity;
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
recorded: true,
|
|
103
|
+
newTotal,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Mock handler for CheckFeatureAccessContract.
|
|
109
|
+
*/
|
|
110
|
+
export async function mockCheckFeatureAccessHandler(
|
|
111
|
+
input: CheckFeatureAccessInput
|
|
112
|
+
): Promise<CheckFeatureAccessOutput> {
|
|
113
|
+
const { feature } = input;
|
|
114
|
+
|
|
115
|
+
const featureMap: Record<string, CheckFeatureAccessOutput> = {
|
|
116
|
+
custom_domains: {
|
|
117
|
+
allowed: true,
|
|
118
|
+
},
|
|
119
|
+
api_access: {
|
|
120
|
+
allowed: true,
|
|
121
|
+
currentUsage: MOCK_USAGE_SUMMARY.apiCalls.total,
|
|
122
|
+
limit: MOCK_USAGE_SUMMARY.apiCalls.limit,
|
|
123
|
+
},
|
|
124
|
+
advanced_analytics: {
|
|
125
|
+
allowed: false,
|
|
126
|
+
reason: 'FEATURE_NOT_INCLUDED',
|
|
127
|
+
},
|
|
128
|
+
unlimited_projects: {
|
|
129
|
+
allowed: false,
|
|
130
|
+
reason: 'PLAN_LIMIT',
|
|
131
|
+
currentUsage: MOCK_SUBSCRIPTION.usage.projects,
|
|
132
|
+
limit: MOCK_SUBSCRIPTION.limits.projects,
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
return featureMap[feature] ?? { allowed: true };
|
|
137
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { defineCommand, defineQuery } from '@contractspec/lib.contracts';
|
|
2
|
+
import {
|
|
3
|
+
CheckFeatureAccessInputModel,
|
|
4
|
+
CheckFeatureAccessOutputModel,
|
|
5
|
+
GetUsageSummaryInputModel,
|
|
6
|
+
GetUsageSummaryOutputModel,
|
|
7
|
+
RecordUsageInputModel,
|
|
8
|
+
RecordUsageOutputModel,
|
|
9
|
+
SubscriptionModel,
|
|
10
|
+
UsageRecordedPayloadModel,
|
|
11
|
+
} from './billing.schema';
|
|
12
|
+
|
|
13
|
+
const OWNERS = ['@example.saas-boilerplate'] as const;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Get subscription status.
|
|
17
|
+
*/
|
|
18
|
+
export const GetSubscriptionContract = defineQuery({
|
|
19
|
+
meta: {
|
|
20
|
+
key: 'saas.billing.subscription.get',
|
|
21
|
+
version: '1.0.0',
|
|
22
|
+
stability: 'stable',
|
|
23
|
+
owners: [...OWNERS],
|
|
24
|
+
tags: ['saas', 'billing', 'subscription'],
|
|
25
|
+
description: 'Get organization subscription status.',
|
|
26
|
+
goal: 'Show current plan and billing status.',
|
|
27
|
+
context: 'Billing page, plan upgrade prompts.',
|
|
28
|
+
},
|
|
29
|
+
io: {
|
|
30
|
+
input: null,
|
|
31
|
+
output: SubscriptionModel,
|
|
32
|
+
},
|
|
33
|
+
policy: {
|
|
34
|
+
auth: 'user',
|
|
35
|
+
},
|
|
36
|
+
acceptance: {
|
|
37
|
+
scenarios: [
|
|
38
|
+
{
|
|
39
|
+
key: 'get-subscription-happy-path',
|
|
40
|
+
given: ['Organization has active subscription'],
|
|
41
|
+
when: ['User requests subscription status'],
|
|
42
|
+
then: ['Subscription details are returned'],
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
examples: [
|
|
46
|
+
{
|
|
47
|
+
key: 'get-basic',
|
|
48
|
+
input: null,
|
|
49
|
+
output: {
|
|
50
|
+
plan: 'pro',
|
|
51
|
+
status: 'active',
|
|
52
|
+
currentPeriodEnd: '2025-02-01T00:00:00Z',
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Record feature usage.
|
|
61
|
+
*/
|
|
62
|
+
export const RecordUsageContract = defineCommand({
|
|
63
|
+
meta: {
|
|
64
|
+
key: 'saas.billing.usage.record',
|
|
65
|
+
version: '1.0.0',
|
|
66
|
+
stability: 'stable',
|
|
67
|
+
owners: [...OWNERS],
|
|
68
|
+
tags: ['saas', 'billing', 'usage'],
|
|
69
|
+
description: 'Record usage of a metered feature.',
|
|
70
|
+
goal: 'Track feature usage for billing.',
|
|
71
|
+
context: 'Called by services when metered features are used.',
|
|
72
|
+
},
|
|
73
|
+
io: {
|
|
74
|
+
input: RecordUsageInputModel,
|
|
75
|
+
output: RecordUsageOutputModel,
|
|
76
|
+
},
|
|
77
|
+
policy: {
|
|
78
|
+
auth: 'user',
|
|
79
|
+
},
|
|
80
|
+
sideEffects: {
|
|
81
|
+
emits: [
|
|
82
|
+
{
|
|
83
|
+
key: 'billing.usage.recorded',
|
|
84
|
+
version: '1.0.0',
|
|
85
|
+
when: 'Usage is recorded',
|
|
86
|
+
payload: UsageRecordedPayloadModel,
|
|
87
|
+
},
|
|
88
|
+
],
|
|
89
|
+
},
|
|
90
|
+
acceptance: {
|
|
91
|
+
scenarios: [
|
|
92
|
+
{
|
|
93
|
+
key: 'record-usage-happy-path',
|
|
94
|
+
given: ['Organization exists'],
|
|
95
|
+
when: ['System records feature usage'],
|
|
96
|
+
then: ['Usage is recorded'],
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
examples: [
|
|
100
|
+
{
|
|
101
|
+
key: 'record-api-call',
|
|
102
|
+
input: { feature: 'api_calls', quantity: 1, idempotencyKey: 'abc-123' },
|
|
103
|
+
output: { recorded: true, currentUsage: 100 },
|
|
104
|
+
},
|
|
105
|
+
],
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Get usage summary.
|
|
111
|
+
*/
|
|
112
|
+
export const GetUsageSummaryContract = defineQuery({
|
|
113
|
+
meta: {
|
|
114
|
+
key: 'saas.billing.usage.summary',
|
|
115
|
+
version: '1.0.0',
|
|
116
|
+
stability: 'stable',
|
|
117
|
+
owners: [...OWNERS],
|
|
118
|
+
tags: ['saas', 'billing', 'usage'],
|
|
119
|
+
description: 'Get usage summary for the current billing period.',
|
|
120
|
+
goal: 'Show usage vs limits.',
|
|
121
|
+
context: 'Billing page, usage dashboards.',
|
|
122
|
+
},
|
|
123
|
+
io: {
|
|
124
|
+
input: GetUsageSummaryInputModel,
|
|
125
|
+
output: GetUsageSummaryOutputModel,
|
|
126
|
+
},
|
|
127
|
+
policy: {
|
|
128
|
+
auth: 'user',
|
|
129
|
+
},
|
|
130
|
+
acceptance: {
|
|
131
|
+
scenarios: [
|
|
132
|
+
{
|
|
133
|
+
key: 'get-usage-happy-path',
|
|
134
|
+
given: ['Organization has usage history'],
|
|
135
|
+
when: ['User requests usage summary'],
|
|
136
|
+
then: ['Usage metrics are returned'],
|
|
137
|
+
},
|
|
138
|
+
],
|
|
139
|
+
examples: [
|
|
140
|
+
{
|
|
141
|
+
key: 'get-current-usage',
|
|
142
|
+
input: { period: 'current' },
|
|
143
|
+
output: { features: [{ name: 'api_calls', used: 100, limit: 1000 }] },
|
|
144
|
+
},
|
|
145
|
+
],
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Check feature access.
|
|
151
|
+
*/
|
|
152
|
+
export const CheckFeatureAccessContract = defineQuery({
|
|
153
|
+
meta: {
|
|
154
|
+
key: 'saas.billing.feature.check',
|
|
155
|
+
version: '1.0.0',
|
|
156
|
+
stability: 'stable',
|
|
157
|
+
owners: [...OWNERS],
|
|
158
|
+
tags: ['saas', 'billing', 'feature'],
|
|
159
|
+
description: 'Check if organization has access to a feature.',
|
|
160
|
+
goal: 'Gate features based on plan/usage.',
|
|
161
|
+
context: 'Feature access checks, upgrade prompts.',
|
|
162
|
+
},
|
|
163
|
+
io: {
|
|
164
|
+
input: CheckFeatureAccessInputModel,
|
|
165
|
+
output: CheckFeatureAccessOutputModel,
|
|
166
|
+
},
|
|
167
|
+
policy: {
|
|
168
|
+
auth: 'user',
|
|
169
|
+
},
|
|
170
|
+
acceptance: {
|
|
171
|
+
scenarios: [
|
|
172
|
+
{
|
|
173
|
+
key: 'check-access-granted',
|
|
174
|
+
given: ['Organization is on Pro plan'],
|
|
175
|
+
when: ['User checks access to Pro feature'],
|
|
176
|
+
then: ['Access is granted'],
|
|
177
|
+
},
|
|
178
|
+
],
|
|
179
|
+
examples: [
|
|
180
|
+
{
|
|
181
|
+
key: 'check-advanced-reports',
|
|
182
|
+
input: { feature: 'advanced_reports' },
|
|
183
|
+
output: { hasAccess: true, reason: 'Included in Pro plan' },
|
|
184
|
+
},
|
|
185
|
+
],
|
|
186
|
+
},
|
|
187
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { definePresentation, StabilityEnum } from '@contractspec/lib.contracts';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Presentation for subscription overview.
|
|
5
|
+
*/
|
|
6
|
+
export const SubscriptionPresentation = definePresentation({
|
|
7
|
+
meta: {
|
|
8
|
+
key: 'saas.billing.subscription',
|
|
9
|
+
version: '1.0.0',
|
|
10
|
+
title: 'Subscription Status',
|
|
11
|
+
description:
|
|
12
|
+
'Subscription status with plan info, limits, and current usage',
|
|
13
|
+
domain: 'saas-boilerplate',
|
|
14
|
+
owners: ['@saas-team'],
|
|
15
|
+
tags: ['billing', 'subscription'],
|
|
16
|
+
stability: StabilityEnum.Beta,
|
|
17
|
+
goal: 'View subscription plan and status',
|
|
18
|
+
context: 'Billing section',
|
|
19
|
+
},
|
|
20
|
+
source: {
|
|
21
|
+
type: 'component',
|
|
22
|
+
framework: 'react',
|
|
23
|
+
componentKey: 'SubscriptionView',
|
|
24
|
+
},
|
|
25
|
+
targets: ['react', 'markdown'],
|
|
26
|
+
policy: {
|
|
27
|
+
flags: ['saas.billing.enabled'],
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Presentation for usage dashboard.
|
|
33
|
+
*/
|
|
34
|
+
export const UsageDashboardPresentation = definePresentation({
|
|
35
|
+
meta: {
|
|
36
|
+
key: 'saas.billing.usage',
|
|
37
|
+
version: '1.0.0',
|
|
38
|
+
title: 'Usage Dashboard',
|
|
39
|
+
description: 'Usage metrics and breakdown by resource type',
|
|
40
|
+
domain: 'saas-boilerplate',
|
|
41
|
+
owners: ['@saas-team'],
|
|
42
|
+
tags: ['billing', 'usage', 'metrics'],
|
|
43
|
+
stability: StabilityEnum.Beta,
|
|
44
|
+
goal: 'Monitor feature usage and limits',
|
|
45
|
+
context: 'Billing section',
|
|
46
|
+
},
|
|
47
|
+
source: {
|
|
48
|
+
type: 'component',
|
|
49
|
+
framework: 'react',
|
|
50
|
+
componentKey: 'UsageDashboardView',
|
|
51
|
+
},
|
|
52
|
+
targets: ['react', 'markdown'],
|
|
53
|
+
policy: {
|
|
54
|
+
flags: ['saas.billing.enabled'],
|
|
55
|
+
},
|
|
56
|
+
});
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { defineSchemaModel, ScalarTypeEnum } from '@contractspec/lib.schema';
|
|
2
|
+
import {
|
|
3
|
+
SubscriptionStatusSchemaEnum,
|
|
4
|
+
FeatureAccessReasonEnum,
|
|
5
|
+
} from './billing.enum';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Organization subscription details schema.
|
|
9
|
+
*/
|
|
10
|
+
export const SubscriptionModel = defineSchemaModel({
|
|
11
|
+
name: 'Subscription',
|
|
12
|
+
description: 'Organization subscription details',
|
|
13
|
+
fields: {
|
|
14
|
+
id: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
|
|
15
|
+
organizationId: {
|
|
16
|
+
type: ScalarTypeEnum.String_unsecure(),
|
|
17
|
+
isOptional: false,
|
|
18
|
+
},
|
|
19
|
+
planId: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
|
|
20
|
+
planName: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
|
|
21
|
+
status: { type: SubscriptionStatusSchemaEnum, isOptional: false },
|
|
22
|
+
currentPeriodStart: { type: ScalarTypeEnum.DateTime(), isOptional: false },
|
|
23
|
+
currentPeriodEnd: { type: ScalarTypeEnum.DateTime(), isOptional: false },
|
|
24
|
+
trialEndsAt: { type: ScalarTypeEnum.DateTime(), isOptional: true },
|
|
25
|
+
cancelAtPeriodEnd: { type: ScalarTypeEnum.Boolean(), isOptional: false },
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Usage summary for a feature schema.
|
|
31
|
+
*/
|
|
32
|
+
export const UsageSummaryModel = defineSchemaModel({
|
|
33
|
+
name: 'UsageSummary',
|
|
34
|
+
description: 'Usage summary for a feature',
|
|
35
|
+
fields: {
|
|
36
|
+
feature: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
|
|
37
|
+
used: { type: ScalarTypeEnum.Int_unsecure(), isOptional: false },
|
|
38
|
+
limit: { type: ScalarTypeEnum.Int_unsecure(), isOptional: true },
|
|
39
|
+
unit: { type: ScalarTypeEnum.String_unsecure(), isOptional: true },
|
|
40
|
+
percentage: { type: ScalarTypeEnum.Float_unsecure(), isOptional: true },
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Input for recording feature usage.
|
|
46
|
+
*/
|
|
47
|
+
export const RecordUsageInputModel = defineSchemaModel({
|
|
48
|
+
name: 'RecordUsageInput',
|
|
49
|
+
description: 'Input for recording feature usage',
|
|
50
|
+
fields: {
|
|
51
|
+
feature: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
|
|
52
|
+
quantity: { type: ScalarTypeEnum.Int_unsecure(), isOptional: false },
|
|
53
|
+
sourceId: { type: ScalarTypeEnum.String_unsecure(), isOptional: true },
|
|
54
|
+
sourceType: { type: ScalarTypeEnum.String_unsecure(), isOptional: true },
|
|
55
|
+
metadata: { type: ScalarTypeEnum.JSONObject(), isOptional: true },
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Output for recording feature usage.
|
|
61
|
+
*/
|
|
62
|
+
export const RecordUsageOutputModel = defineSchemaModel({
|
|
63
|
+
name: 'RecordUsageOutput',
|
|
64
|
+
description: 'Output for recording feature usage',
|
|
65
|
+
fields: {
|
|
66
|
+
recorded: { type: ScalarTypeEnum.Boolean(), isOptional: false },
|
|
67
|
+
currentUsage: { type: ScalarTypeEnum.Int_unsecure(), isOptional: false },
|
|
68
|
+
limit: { type: ScalarTypeEnum.Int_unsecure(), isOptional: true },
|
|
69
|
+
limitReached: { type: ScalarTypeEnum.Boolean(), isOptional: false },
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Payload for usage.recorded event.
|
|
75
|
+
*/
|
|
76
|
+
export const UsageRecordedPayloadModel = defineSchemaModel({
|
|
77
|
+
name: 'UsageRecordedPayload',
|
|
78
|
+
description: 'Payload for usage.recorded event',
|
|
79
|
+
fields: {
|
|
80
|
+
feature: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
|
|
81
|
+
quantity: { type: ScalarTypeEnum.Int_unsecure(), isOptional: false },
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Input for getting usage summary.
|
|
87
|
+
*/
|
|
88
|
+
export const GetUsageSummaryInputModel = defineSchemaModel({
|
|
89
|
+
name: 'GetUsageSummaryInput',
|
|
90
|
+
description: 'Input for getting usage summary',
|
|
91
|
+
fields: {
|
|
92
|
+
billingPeriod: { type: ScalarTypeEnum.String_unsecure(), isOptional: true },
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Output for usage summary.
|
|
98
|
+
*/
|
|
99
|
+
export const GetUsageSummaryOutputModel = defineSchemaModel({
|
|
100
|
+
name: 'GetUsageSummaryOutput',
|
|
101
|
+
description: 'Output for usage summary',
|
|
102
|
+
fields: {
|
|
103
|
+
billingPeriod: {
|
|
104
|
+
type: ScalarTypeEnum.String_unsecure(),
|
|
105
|
+
isOptional: false,
|
|
106
|
+
},
|
|
107
|
+
usage: { type: UsageSummaryModel, isArray: true, isOptional: false },
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Input for checking feature access.
|
|
113
|
+
*/
|
|
114
|
+
export const CheckFeatureAccessInputModel = defineSchemaModel({
|
|
115
|
+
name: 'CheckFeatureAccessInput',
|
|
116
|
+
description: 'Input for checking feature access',
|
|
117
|
+
fields: {
|
|
118
|
+
feature: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Output for feature access check.
|
|
124
|
+
*/
|
|
125
|
+
export const CheckFeatureAccessOutputModel = defineSchemaModel({
|
|
126
|
+
name: 'CheckFeatureAccessOutput',
|
|
127
|
+
description: 'Output for feature access check',
|
|
128
|
+
fields: {
|
|
129
|
+
hasAccess: { type: ScalarTypeEnum.Boolean(), isOptional: false },
|
|
130
|
+
reason: { type: FeatureAccessReasonEnum, isOptional: true },
|
|
131
|
+
upgradeUrl: { type: ScalarTypeEnum.URL(), isOptional: true },
|
|
132
|
+
},
|
|
133
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Billing domain - subscription, usage tracking, and feature access.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// Enums
|
|
6
|
+
export {
|
|
7
|
+
SubscriptionStatusSchemaEnum,
|
|
8
|
+
FeatureAccessReasonEnum,
|
|
9
|
+
} from './billing.enum';
|
|
10
|
+
|
|
11
|
+
// Schema models
|
|
12
|
+
export {
|
|
13
|
+
SubscriptionModel,
|
|
14
|
+
UsageSummaryModel,
|
|
15
|
+
RecordUsageInputModel,
|
|
16
|
+
RecordUsageOutputModel,
|
|
17
|
+
UsageRecordedPayloadModel,
|
|
18
|
+
GetUsageSummaryInputModel,
|
|
19
|
+
GetUsageSummaryOutputModel,
|
|
20
|
+
CheckFeatureAccessInputModel,
|
|
21
|
+
CheckFeatureAccessOutputModel,
|
|
22
|
+
} from './billing.schema';
|
|
23
|
+
|
|
24
|
+
// Contracts
|
|
25
|
+
export {
|
|
26
|
+
GetSubscriptionContract,
|
|
27
|
+
RecordUsageContract,
|
|
28
|
+
GetUsageSummaryContract,
|
|
29
|
+
CheckFeatureAccessContract,
|
|
30
|
+
} from './billing.operations';
|
|
31
|
+
|
|
32
|
+
// Events
|
|
33
|
+
export {
|
|
34
|
+
UsageRecordedEvent,
|
|
35
|
+
UsageLimitReachedEvent,
|
|
36
|
+
SubscriptionChangedEvent,
|
|
37
|
+
} from './billing.event';
|
|
38
|
+
|
|
39
|
+
// Entities
|
|
40
|
+
export {
|
|
41
|
+
SubscriptionStatusEnum,
|
|
42
|
+
SubscriptionEntity,
|
|
43
|
+
BillingUsageEntity,
|
|
44
|
+
UsageLimitEntity,
|
|
45
|
+
} from './billing.entity';
|
|
46
|
+
|
|
47
|
+
// Presentations
|
|
48
|
+
export {
|
|
49
|
+
SubscriptionPresentation,
|
|
50
|
+
UsageDashboardPresentation,
|
|
51
|
+
} from './billing.presentation';
|
|
52
|
+
|
|
53
|
+
// Handlers
|
|
54
|
+
export {
|
|
55
|
+
mockGetSubscriptionHandler,
|
|
56
|
+
mockGetUsageSummaryHandler,
|
|
57
|
+
mockRecordUsageHandler,
|
|
58
|
+
mockCheckFeatureAccessHandler,
|
|
59
|
+
type Subscription,
|
|
60
|
+
type UsageSummary,
|
|
61
|
+
type RecordUsageInput,
|
|
62
|
+
type CheckFeatureAccessInput,
|
|
63
|
+
type CheckFeatureAccessOutput,
|
|
64
|
+
} from './billing.handler';
|