@taskforcehq/taskforce 0.3.303 → 0.3.305
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 +140 -247
- package/dist/Taskforce.module.css +102 -5
- package/dist/TaskforceCore.js +635 -238
- package/dist/TaskforceCore.test.js +2243 -495
- package/dist/annotatedAttachments/service.d.ts +1 -1
- package/dist/annotatedAttachments/types.d.ts +1 -1
- package/dist/compat/workspaceSyncCompat.js +6 -0
- package/dist/components/features/AgentsModule.js +1 -1
- package/dist/components/features/AgentsModule.test.js +1 -1
- package/dist/components/features/DocumentIndex.d.ts +1 -1
- package/dist/components/features/DocumentIndex.js +1 -1
- package/dist/components/features/DocumentViewer.d.ts +1 -1
- package/dist/components/features/DocumentViewer.js +32 -9
- package/dist/components/features/DocumentWorkspace.d.ts +5 -22
- package/dist/components/features/DocumentWorkspace.js +12 -155
- package/dist/components/features/DocumentWorkspace.test.js +193 -6
- package/dist/components/features/TaskSettings.js +87 -41
- package/dist/components/features/TaskSettings.test.js +180 -40
- package/dist/components/features/documentWorkspaceModel.d.ts +24 -0
- package/dist/components/features/documentWorkspaceModel.js +57 -0
- package/dist/components/features/useDocumentWorkspaceController.d.ts +34 -0
- package/dist/components/features/useDocumentWorkspaceController.js +113 -0
- package/dist/components/task/TaskActionHeader.d.ts +2 -1
- package/dist/components/task/TaskActionHeader.js +3 -3
- package/dist/components/task/TaskActionHeader.test.js +8 -0
- package/dist/components/task/TaskCard.js +16 -1
- package/dist/components/task/TaskCard.test.js +31 -0
- package/dist/components/task/TaskForm.d.ts +9 -8
- package/dist/components/task/TaskForm.js +374 -219
- package/dist/components/task/TaskForm.test.js +482 -361
- package/dist/components/task/TaskKanban.d.ts +1 -1
- package/dist/components/task/TaskKanban.js +33 -161
- package/dist/components/task/TaskStatusActions.d.ts +6 -1
- package/dist/components/task/TaskStatusActions.js +8 -3
- package/dist/components/task/TaskStatusActions.test.js +9 -0
- package/dist/components/task/taskKanbanDropRules.d.ts +58 -0
- package/dist/components/task/taskKanbanDropRules.js +187 -0
- package/dist/components/task/taskKanbanDropRules.test.d.ts +1 -0
- package/dist/components/task/taskKanbanDropRules.test.js +114 -0
- package/dist/components/ui/Modal.d.ts +1 -1
- package/dist/components/ui/Modal.js +1 -0
- package/dist/components/views/AppShellHeader.js +5 -1
- package/dist/components/views/AppShellHeader.test.js +4 -0
- package/dist/components/views/PlanComparisonPage.d.ts +3 -2
- package/dist/components/views/PlanComparisonPage.js +9 -10
- package/dist/components/views/PlanComparisonPage.test.js +55 -41
- package/dist/components/views/PlansPage.d.ts +5 -1
- package/dist/components/views/PlansPage.js +150 -81
- package/dist/components/views/PlansPage.test.js +367 -41
- package/dist/components/views/StandaloneLayout.js +135 -54
- package/dist/components/views/WidgetView.js +11 -2
- package/dist/components/views/panels/FilterToolbar.test.js +6 -4
- package/dist/components/views/standalone/modals/AccountHubModal.d.ts +2 -14
- package/dist/components/views/standalone/modals/AccountHubModal.js +12 -14
- package/dist/components/views/standalone/modals/AccountHubModal.test.js +24 -1
- package/dist/components/views/standalone/modals/SyncStatusModal.js +1 -1
- package/dist/config/envSchema.js +1 -1
- package/dist/core/AiProfileService.d.ts +6 -1
- package/dist/core/AiProfileService.js +161 -16
- package/dist/core/AiProfileService.test.js +3 -1
- package/dist/core/AiProfiles.test.js +200 -0
- package/dist/core/AuthTokenService.test.d.ts +1 -0
- package/dist/core/AuthTokenService.test.js +78 -0
- package/dist/core/DeletedTaskLifecycleService.d.ts +2 -1
- package/dist/core/EntitlementsPolicy.test.js +75 -13
- package/dist/core/PlanEntitlementService.d.ts +9 -1
- package/dist/core/PlanEntitlementService.js +144 -19
- package/dist/core/PlanVersionPolicy.test.js +19 -26
- package/dist/core/SignupMonetizationSettings.test.js +75 -15
- package/dist/core/SystemAdmin.test.js +212 -56
- package/dist/core/TaskActivityService.d.ts +4 -1
- package/dist/core/TaskActivityService.js +45 -0
- package/dist/core/TaskAuditTimeline.test.js +52 -1
- package/dist/core/TaskTaxonomyValidation.test.js +53 -0
- package/dist/core/Taskforce.d.ts +18 -11
- package/dist/core/Taskforce.js +93 -12
- package/dist/core/WorkspacePlanMode.test.js +73 -8
- package/dist/core/shared.d.ts +2 -0
- package/dist/core/shared.js +11 -0
- package/dist/documentReviews/service.d.ts +1 -1
- package/dist/documentReviews/types.d.ts +1 -1
- package/dist/hooks/auth/useTaskforceAuthBootstrap.d.ts +48 -0
- package/dist/hooks/auth/useTaskforceAuthBootstrap.js +219 -0
- package/dist/hooks/sync/auth.d.ts +37 -0
- package/dist/hooks/sync/auth.js +63 -0
- package/dist/hooks/sync/bootstrap.d.ts +34 -0
- package/dist/hooks/sync/bootstrap.js +52 -0
- package/dist/hooks/sync/orchestratorShared.d.ts +36 -0
- package/dist/hooks/sync/orchestratorShared.js +209 -0
- package/dist/hooks/sync/orchestratorShared.test.d.ts +1 -0
- package/dist/hooks/sync/orchestratorShared.test.js +49 -0
- package/dist/hooks/sync/realtime.d.ts +25 -0
- package/dist/hooks/sync/realtime.js +60 -0
- package/dist/hooks/sync/recovery.d.ts +69 -0
- package/dist/hooks/sync/recovery.js +118 -0
- package/dist/hooks/sync/transfers.d.ts +123 -0
- package/dist/hooks/sync/transfers.js +435 -0
- package/dist/hooks/sync/useDataVersionRefresh.d.ts +16 -0
- package/dist/hooks/sync/useDataVersionRefresh.js +73 -0
- package/dist/hooks/ui/useResourceExport.d.ts +3 -1
- package/dist/hooks/ui/useResourceExport.js +5 -2
- package/dist/hooks/ui/useWorkflowTemplates.js +11 -6
- package/dist/hooks/useRealtimeSync.js +2 -1
- package/dist/hooks/useSyncOrchestrator.aiProfiles.test.js +8 -0
- package/dist/hooks/useSyncOrchestrator.context-assets.test.d.ts +1 -0
- package/dist/hooks/useSyncOrchestrator.context-assets.test.js +228 -0
- package/dist/hooks/useSyncOrchestrator.d.ts +12 -8
- package/dist/hooks/useSyncOrchestrator.js +388 -879
- package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +641 -6
- package/dist/hooks/useTaskMutations.d.ts +2 -1
- package/dist/hooks/useTaskMutations.js +57 -7
- package/dist/hooks/useTaskMutations.test.js +176 -0
- package/dist/hooks/useTaskforce.d.ts +13 -4
- package/dist/hooks/useTaskforce.js +302 -817
- package/dist/hooks/useTaskforce.sync-behavior.test.js +78 -0
- package/dist/hooks/useWorkspaceSyncController.d.ts +4 -1
- package/dist/hooks/useWorkspaceSyncController.js +33 -11
- package/dist/hooks/useWorkspaceSyncController.test.js +1 -0
- package/dist/hooks/workspace/useTaskforceApiRouting.test.js +17 -0
- package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.d.ts +166 -0
- package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.js +434 -0
- package/dist/hooks/workspace/workspaceLocalState.d.ts +19 -0
- package/dist/hooks/workspace/workspaceLocalState.js +100 -0
- package/dist/hooks/workspace/workspaceLocalState.test.d.ts +1 -0
- package/dist/hooks/workspace/workspaceLocalState.test.js +59 -0
- package/dist/localization/locales/en-US.js +2 -2
- package/dist/mcp/adminWorkspaceRegistrar.d.ts +2 -0
- package/dist/mcp/adminWorkspaceRegistrar.js +154 -0
- package/dist/mcp/canonicalAssetHelpers.d.ts +70 -0
- package/dist/mcp/canonicalAssetHelpers.js +259 -0
- package/dist/mcp/clientIntegrations.d.ts +12 -1
- package/dist/mcp/clientIntegrations.js +161 -6
- package/dist/mcp/clientIntegrations.test.js +76 -4
- package/dist/mcp/collaborationRegistrar.d.ts +2 -0
- package/dist/mcp/collaborationRegistrar.js +71 -0
- package/dist/mcp/documentAssetRegistrar.d.ts +2 -0
- package/dist/mcp/documentAssetRegistrar.js +346 -0
- package/dist/mcp/documentHelpers.d.ts +100 -0
- package/dist/mcp/documentHelpers.js +161 -0
- package/dist/mcp/runtime.js +2455 -3925
- package/dist/mcp/runtime.test.js +240 -8
- package/dist/mcp/taskAttachmentHelpers.d.ts +41 -0
- package/dist/mcp/taskAttachmentHelpers.js +60 -0
- package/dist/mcp/taskAttachmentTypes.d.ts +37 -0
- package/dist/mcp/taskAttachmentTypes.js +1 -0
- package/dist/mcp/taskPlanningRegistrar.d.ts +2 -0
- package/dist/mcp/taskPlanningRegistrar.js +418 -0
- package/dist/mcp/toolCatalog.d.ts +35 -0
- package/dist/mcp/toolCatalog.js +3 -0
- package/dist/plugins/vite.js +1 -1
- package/dist/runtime/appGateDefinitions.d.ts +1 -1
- package/dist/runtime/appGateDefinitions.js +1 -1
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +46 -3
- package/dist/server/index.test.js +12 -1
- package/dist/server/routes/admin.d.ts +1 -5
- package/dist/server/routes/admin.js +151 -80
- package/dist/server/routes/annotatedAttachments.d.ts +1 -1
- package/dist/server/routes/annotatedAttachments.js +1 -1
- package/dist/server/routes/auth.d.ts +1 -4
- package/dist/server/routes/auth.js +59 -64
- package/dist/server/routes/authSupport.d.ts +30 -0
- package/dist/server/routes/authSupport.js +81 -0
- package/dist/server/routes/billing.d.ts +1 -1
- package/dist/server/routes/billing.js +122 -255
- package/dist/server/routes/billing.test.js +257 -86
- package/dist/server/routes/documentReviews.d.ts +1 -1
- package/dist/server/routes/documentReviews.js +1 -1
- package/dist/server/routes/documents.d.ts +4 -2
- package/dist/server/routes/documents.js +73 -7
- package/dist/server/routes/primitives.d.ts +11 -0
- package/dist/server/routes/primitives.js +73 -0
- package/dist/server/routes/resources.d.ts +1 -1
- package/dist/server/routes/resources.js +1 -1
- package/dist/server/routes/shared.d.ts +31 -1
- package/dist/server/routes/shared.js +26 -0
- package/dist/server/routes/sync.d.ts +1 -1
- package/dist/server/routes/sync.integration.test.js +20 -0
- package/dist/server/routes/sync.js +111 -238
- package/dist/server/routes/tasks.d.ts +1 -1
- package/dist/server/routes/tasks.js +50 -8
- package/dist/server/routes/workspaces.d.ts +1 -1
- package/dist/server/routes/workspaces.js +1 -1
- package/dist/server/routes.d.ts +2 -9
- package/dist/server/routes.js +84 -181
- package/dist/server/routes.test.js +912 -32
- package/dist/shared/taskActor.d.ts +10 -0
- package/dist/shared/taskActor.js +1 -0
- package/dist/sync/collaborationSyncPayload.js +4 -0
- package/dist/sync/collaborationSyncPayload.test.js +8 -0
- package/dist/sync/collaborationSyncState.d.ts +3 -1
- package/dist/sync/collaborationSyncState.js +19 -0
- package/dist/sync/contentSyncState.js +28 -17
- package/dist/sync/syncApplyHandlers.d.ts +87 -1
- package/dist/sync/syncApplyHandlers.js +370 -8
- package/dist/sync/syncApplyHandlers.test.d.ts +1 -0
- package/dist/sync/syncApplyHandlers.test.js +379 -0
- package/dist/sync/syncService.d.ts +9 -1
- package/dist/sync/syncService.js +44 -2
- package/dist/sync/syncService.test.js +62 -0
- package/dist/sync/workspacePullFeed.d.ts +1 -0
- package/dist/sync/workspacePullFeed.js +66 -59
- package/dist/sync/workspacePullFeed.test.js +72 -4
- package/dist/sync/workspaceSyncModel.d.ts +25 -1
- package/dist/sync/workspaceSyncModel.js +77 -3
- package/dist/sync/workspaceSyncModel.test.js +153 -0
- package/dist/sync/workspaceSyncState.d.ts +2 -0
- package/dist/sync/workspaceSyncState.js +1 -0
- package/dist/types.d.ts +2 -10
- package/dist/ui/assets/AgentsModule-N2MnQBZk.js +1 -0
- package/dist/ui/assets/{AnnotatedAttachmentWorkspace-C_TmXZwA.js → AnnotatedAttachmentWorkspace-BG6P_GVW.js} +1 -1
- package/dist/ui/assets/DocumentWorkspace-B3nV_Gc5.css +1 -0
- package/dist/ui/assets/DocumentWorkspace-DRbDMwh8.js +252 -0
- package/dist/ui/assets/{InitiativesModule-4-Rh2K2n.js → InitiativesModule-Dhm7M8e7.js} +1 -1
- package/dist/ui/assets/{PlansPage-BlVC_lRq.css → PlansPage-C2dd2n1X.css} +1 -1
- package/dist/ui/assets/PlansPage-Cq0zXj3I.js +1 -0
- package/dist/ui/assets/TaskSettings-ln0aF7xc.js +9 -0
- package/dist/ui/assets/{WorkflowsModule-CGk9s3NJ.js → WorkflowsModule-BcS4afRn.js} +1 -1
- package/dist/ui/assets/index-C2-OXZV7.css +1 -0
- package/dist/ui/assets/index-DcSI5F86.js +6 -0
- package/dist/ui/assets/{vendor-icons-pWocEipT.js → vendor-icons-BSUaRwF8.js} +1 -1
- package/dist/ui/index.html +4 -4
- package/dist/utils/commercialLifecyclePresentation.d.ts +19 -0
- package/dist/utils/commercialLifecyclePresentation.js +196 -0
- package/dist/utils/taskActivity.js +4 -0
- package/dist/utils/taskActivity.test.js +25 -1
- package/package.json +10 -1
- package/scripts/check-cloud-mcp.mjs +83 -0
- package/scripts/playwright-auth.sh +9 -1
- package/scripts/run-billing-performance-smoke.sh +24 -0
- package/dist/ui/assets/AgentsModule-CpsTmrIW.js +0 -1
- package/dist/ui/assets/DocumentWorkspace-C3GyzrWm.js +0 -250
- package/dist/ui/assets/DocumentWorkspace-C_8T8oz-.css +0 -1
- package/dist/ui/assets/PlansPage-BP7AYOqr.js +0 -1
- package/dist/ui/assets/TaskSettings-BOC5F6Ag.js +0 -8
- package/dist/ui/assets/index-CLIMgR6g.js +0 -6
- package/dist/ui/assets/index-DneETxcu.css +0 -1
|
@@ -3,6 +3,7 @@ import fs from 'fs';
|
|
|
3
3
|
import os from 'os';
|
|
4
4
|
import path from 'path';
|
|
5
5
|
import { TaskforceCore, TaskforceRuleError } from './Taskforce';
|
|
6
|
+
import { WorkspaceAssetStore } from '../storage/workspaceAssetStore.js';
|
|
6
7
|
function makeProjectRoot() {
|
|
7
8
|
return fs.mkdtempSync(path.join(os.tmpdir(), 'taskforce-entitlements-'));
|
|
8
9
|
}
|
|
@@ -20,20 +21,20 @@ function openCore(projectRoot) {
|
|
|
20
21
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
21
22
|
`).run('default', planVersionId, 'month', `free:${planVersionId}:month`, 'free', 1, 0, null, now, now);
|
|
22
23
|
};
|
|
23
|
-
core.upsertPlanCatalog({ planId: '
|
|
24
|
-
addFreeMapping('
|
|
25
|
-
core.upsertPlanCatalog({ planId: '
|
|
26
|
-
core.upsertPlanCatalog({ planId: '
|
|
27
|
-
addFreeMapping('
|
|
28
|
-
core.upsertPlanCatalog({ planId: '
|
|
24
|
+
core.upsertPlanCatalog({ planId: 'free', displayName: 'Free' });
|
|
25
|
+
addFreeMapping('free-v1');
|
|
26
|
+
core.upsertPlanCatalog({ planId: 'free', displayName: 'Free', status: 'active' });
|
|
27
|
+
core.upsertPlanCatalog({ planId: 'team-5', displayName: 'Team 5' });
|
|
28
|
+
addFreeMapping('team-5-v1');
|
|
29
|
+
core.upsertPlanCatalog({ planId: 'team-5', displayName: 'Team 5', status: 'active' });
|
|
29
30
|
core.createPlanVersion({
|
|
30
|
-
planId: '
|
|
31
|
+
planId: 'team-5',
|
|
31
32
|
versionNumber: 2,
|
|
32
33
|
isDefault: true,
|
|
33
34
|
seatLimit: 5
|
|
34
35
|
});
|
|
35
36
|
core.updatePlanVersionFeatures({
|
|
36
|
-
planVersionId: '
|
|
37
|
+
planVersionId: 'team-5-v2',
|
|
37
38
|
features: [
|
|
38
39
|
{ featureKey: 'collaboration.invites', access: 'enabled' },
|
|
39
40
|
{ featureKey: 'collaboration.team_management', access: 'enabled' },
|
|
@@ -139,13 +140,13 @@ describe('Taskforce entitlements policy engine', () => {
|
|
|
139
140
|
email: 'owner@example.com'
|
|
140
141
|
});
|
|
141
142
|
core.createPlanVersion({
|
|
142
|
-
planId: '
|
|
143
|
+
planId: 'team-5',
|
|
143
144
|
versionNumber: 3,
|
|
144
145
|
isDefault: false,
|
|
145
146
|
seatLimit: 2
|
|
146
147
|
});
|
|
147
148
|
core.updatePlanVersionFeatures({
|
|
148
|
-
planVersionId: '
|
|
149
|
+
planVersionId: 'team-5-v3',
|
|
149
150
|
features: [
|
|
150
151
|
{ featureKey: 'collaboration.invites', access: 'enabled' },
|
|
151
152
|
{ featureKey: 'collaboration.team_management', access: 'enabled' },
|
|
@@ -154,7 +155,7 @@ describe('Taskforce entitlements policy engine', () => {
|
|
|
154
155
|
});
|
|
155
156
|
core.updateAccountEntitlement({
|
|
156
157
|
accountId: 'owner-u1',
|
|
157
|
-
planVersionId: '
|
|
158
|
+
planVersionId: 'team-5-v3',
|
|
158
159
|
entitlementState: 'active',
|
|
159
160
|
actorUserId: 'system-admin',
|
|
160
161
|
actorRole: 'system_admin'
|
|
@@ -184,7 +185,7 @@ describe('Taskforce entitlements policy engine', () => {
|
|
|
184
185
|
const core = openCore(projectRoot);
|
|
185
186
|
try {
|
|
186
187
|
core.updatePlanVersionFeatures({
|
|
187
|
-
planVersionId: '
|
|
188
|
+
planVersionId: 'free-v1',
|
|
188
189
|
features: [
|
|
189
190
|
{ featureKey: 'workspace.switching', access: 'limited', config: { maxWorkspaces: 1 } }
|
|
190
191
|
]
|
|
@@ -198,7 +199,7 @@ describe('Taskforce entitlements policy engine', () => {
|
|
|
198
199
|
});
|
|
199
200
|
core.updateAccountEntitlement({
|
|
200
201
|
accountId: 'owner-u1',
|
|
201
|
-
planVersionId: '
|
|
202
|
+
planVersionId: 'free-v1',
|
|
202
203
|
entitlementState: 'active',
|
|
203
204
|
actorUserId: 'system-admin',
|
|
204
205
|
actorRole: 'system_admin'
|
|
@@ -215,4 +216,65 @@ describe('Taskforce entitlements policy engine', () => {
|
|
|
215
216
|
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
216
217
|
}
|
|
217
218
|
});
|
|
219
|
+
it('enforces limited document cloud storage allowances by current usage plus requested upload size', () => {
|
|
220
|
+
const projectRoot = makeProjectRoot();
|
|
221
|
+
const core = openCore(projectRoot);
|
|
222
|
+
try {
|
|
223
|
+
core.updatePlanVersionFeatures({
|
|
224
|
+
planVersionId: 'free-v1',
|
|
225
|
+
features: [
|
|
226
|
+
{ featureKey: 'context.uploads', access: 'limited', config: { storageLimitMb: 1 } }
|
|
227
|
+
]
|
|
228
|
+
});
|
|
229
|
+
core.createWorkspace({ workspaceId: 'workspace-storage', name: 'Storage Workspace' });
|
|
230
|
+
core.ensureBootstrapUserAccess({
|
|
231
|
+
userId: 'owner-u1',
|
|
232
|
+
workspaceId: 'workspace-storage',
|
|
233
|
+
role: 'owner',
|
|
234
|
+
email: 'owner@example.com'
|
|
235
|
+
});
|
|
236
|
+
core.updateAccountEntitlement({
|
|
237
|
+
accountId: 'owner-u1',
|
|
238
|
+
planVersionId: 'free-v1',
|
|
239
|
+
entitlementState: 'active',
|
|
240
|
+
actorUserId: 'system-admin',
|
|
241
|
+
actorRole: 'system_admin'
|
|
242
|
+
});
|
|
243
|
+
const store = new WorkspaceAssetStore(core.getDatabaseAdapter(), core.getTenantId());
|
|
244
|
+
store.upsertAsset({
|
|
245
|
+
assetId: 'asset-storage-1',
|
|
246
|
+
workspaceId: 'workspace-storage',
|
|
247
|
+
kind: 'document',
|
|
248
|
+
originalFilename: 'existing.pdf',
|
|
249
|
+
mimeType: 'application/pdf',
|
|
250
|
+
storageProvider: 'local',
|
|
251
|
+
storageKey: 'workspaces/workspace-storage/assets/documents/asset-storage-1-existing.pdf',
|
|
252
|
+
contentSha256: 'existing-hash',
|
|
253
|
+
sizeBytes: 900_000,
|
|
254
|
+
documentId: 'document-storage-1',
|
|
255
|
+
version: 1,
|
|
256
|
+
isLatest: true
|
|
257
|
+
});
|
|
258
|
+
const withinLimit = core.evaluateFeatureAccess({
|
|
259
|
+
userId: 'owner-u1',
|
|
260
|
+
workspaceId: 'workspace-storage',
|
|
261
|
+
featureKey: 'context.uploads',
|
|
262
|
+
context: { requestedBytes: 100_000 }
|
|
263
|
+
});
|
|
264
|
+
expect(withinLimit.allowed).toBe(true);
|
|
265
|
+
expect(withinLimit.reasonCode).toBe('OK');
|
|
266
|
+
const overLimit = core.evaluateFeatureAccess({
|
|
267
|
+
userId: 'owner-u1',
|
|
268
|
+
workspaceId: 'workspace-storage',
|
|
269
|
+
featureKey: 'context.uploads',
|
|
270
|
+
context: { requestedBytes: 200_000 }
|
|
271
|
+
});
|
|
272
|
+
expect(overLimit.allowed).toBe(false);
|
|
273
|
+
expect(overLimit.reasonCode).toBe('UPLOAD_STORAGE_LIMIT_REACHED');
|
|
274
|
+
}
|
|
275
|
+
finally {
|
|
276
|
+
core.close();
|
|
277
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
278
|
+
}
|
|
279
|
+
});
|
|
218
280
|
});
|
|
@@ -5,7 +5,7 @@ export interface PlanEntitlementServiceDeps {
|
|
|
5
5
|
readGlobalConfig: () => GlobalTaskforceSettings;
|
|
6
6
|
writeGlobalConfig: (config: GlobalTaskforceSettings) => void;
|
|
7
7
|
}
|
|
8
|
-
type CommercialStateTransitionSource = 'registration' | 'checkout-start' | 'checkout-confirm' | 'stripe-webhook' | 'backfill' | 'legacy-plan-mode' | 'admin' | 'system';
|
|
8
|
+
type CommercialStateTransitionSource = 'registration' | 'invite-accept' | 'checkout-start' | 'checkout-confirm' | 'stripe-webhook' | 'backfill' | 'legacy-plan-mode' | 'admin' | 'system';
|
|
9
9
|
/**
|
|
10
10
|
* Handles all plan catalog, plan version, and account entitlement logic.
|
|
11
11
|
* Extracted from Taskforce.ts to reduce monolith size.
|
|
@@ -29,6 +29,7 @@ export declare class PlanEntitlementService {
|
|
|
29
29
|
private readAccountEntitlementRow;
|
|
30
30
|
private normalizeExpiredCheckoutPendingRow;
|
|
31
31
|
private isBillingConfirmedTransitionSource;
|
|
32
|
+
private countWorkspaceStorageUsageBytes;
|
|
32
33
|
private resolveCanonicalTransitionSource;
|
|
33
34
|
private resolvePendingPlanVersionId;
|
|
34
35
|
private buildBlockedAccessStatus;
|
|
@@ -73,6 +74,10 @@ export declare class PlanEntitlementService {
|
|
|
73
74
|
limit: number | null;
|
|
74
75
|
feature: PlanFeatureSnapshot | null;
|
|
75
76
|
};
|
|
77
|
+
resolveAiProfileLimitForWorkspace(workspaceId: string): {
|
|
78
|
+
limit: number | null;
|
|
79
|
+
feature: PlanFeatureSnapshot | null;
|
|
80
|
+
};
|
|
76
81
|
private planHasAnyActivePricing;
|
|
77
82
|
private planVersionHasActiveFreePricing;
|
|
78
83
|
private planVersionIsTrialEligible;
|
|
@@ -92,6 +97,7 @@ export declare class PlanEntitlementService {
|
|
|
92
97
|
createUserBillingRecord(userId: string, options?: {
|
|
93
98
|
entitlementState?: string;
|
|
94
99
|
planId?: string | null;
|
|
100
|
+
effectiveUntil?: string | null;
|
|
95
101
|
}): void;
|
|
96
102
|
getUserEntitlement(userId: string): {
|
|
97
103
|
entitlementState: string;
|
|
@@ -99,12 +105,14 @@ export declare class PlanEntitlementService {
|
|
|
99
105
|
};
|
|
100
106
|
hasAccountEntitlement(accountId: string): boolean;
|
|
101
107
|
private tryGetAccountEntitlement;
|
|
108
|
+
private tryAutoHealLegacyPersonalFreeEntitlement;
|
|
102
109
|
getAccountAccessStatus(accountId: string): AccountAccessStatus;
|
|
103
110
|
getEffectivePlanVersion(accountId: string): PlanVersionSnapshot;
|
|
104
111
|
getAccountEntitlement(accountId: string): AccountEntitlementSnapshot;
|
|
105
112
|
countWorkspaceSeatUsage(workspaceId: string, options?: {
|
|
106
113
|
includeInvited?: boolean;
|
|
107
114
|
}): SeatUsageSnapshot;
|
|
115
|
+
countAiProfileUsage(workspaceId: string): SeatUsageSnapshot;
|
|
108
116
|
assertSeatAvailable(workspaceId: string): void;
|
|
109
117
|
evaluateFeatureAccess(input: {
|
|
110
118
|
userId: string;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { TaskforceRuleError } from './types.js';
|
|
2
|
-
import { DOCUMENT_CLOUD_STORAGE_FEATURE_KEY, WORKSPACES_FEATURE_KEY, DOCUMENT_CLOUD_STORAGE_LIMIT_CONFIG_KEY, WORKSPACES_LIMIT_CONFIG_KEY, DEFAULT_PLAN_FEATURE_CATALOG, PLAN_FEATURE_DEPENDENCIES, generateUuidV7, } from './shared.js';
|
|
2
|
+
import { DOCUMENT_CLOUD_STORAGE_FEATURE_KEY, WORKSPACES_FEATURE_KEY, AI_PROFILES_FEATURE_KEY, DOCUMENT_CLOUD_STORAGE_LIMIT_CONFIG_KEY, WORKSPACES_LIMIT_CONFIG_KEY, AI_PROFILES_LIMIT_CONFIG_KEY, DEFAULT_PLAN_FEATURE_CATALOG, PLAN_FEATURE_DEPENDENCIES, generateUuidV7, } from './shared.js';
|
|
3
3
|
const LEGACY_REMOVED_PLAN_FEATURE_KEYS = Object.freeze([
|
|
4
4
|
'documents.workspace_access'
|
|
5
5
|
]);
|
|
@@ -113,6 +113,15 @@ export class PlanEntitlementService {
|
|
|
113
113
|
[WORKSPACES_LIMIT_CONFIG_KEY]: limit ?? 1
|
|
114
114
|
};
|
|
115
115
|
}
|
|
116
|
+
if (featureKey === AI_PROFILES_FEATURE_KEY) {
|
|
117
|
+
if (access === 'limited') {
|
|
118
|
+
const limit = this.readPositiveIntegerConfig(config[AI_PROFILES_LIMIT_CONFIG_KEY], 1);
|
|
119
|
+
return {
|
|
120
|
+
[AI_PROFILES_LIMIT_CONFIG_KEY]: limit ?? 1
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
return {};
|
|
124
|
+
}
|
|
116
125
|
return config && typeof config === 'object' ? { ...config } : {};
|
|
117
126
|
}
|
|
118
127
|
isEntitlementStateAllowed(state) {
|
|
@@ -152,13 +161,28 @@ export class PlanEntitlementService {
|
|
|
152
161
|
`).run('pending_plan_selection', 'checkout-expired', now, this.tenantId, accountId);
|
|
153
162
|
this.createUserBillingRecord(accountId, {
|
|
154
163
|
entitlementState: 'pending_plan_selection',
|
|
155
|
-
planId: null
|
|
164
|
+
planId: null,
|
|
165
|
+
effectiveUntil: null
|
|
156
166
|
});
|
|
157
167
|
return this.readAccountEntitlementRow(accountId);
|
|
158
168
|
}
|
|
159
169
|
isBillingConfirmedTransitionSource(source) {
|
|
160
170
|
return source === 'checkout-confirm' || source === 'stripe-webhook';
|
|
161
171
|
}
|
|
172
|
+
countWorkspaceStorageUsageBytes(workspaceIdRaw) {
|
|
173
|
+
const workspaceId = String(workspaceIdRaw || '').trim();
|
|
174
|
+
if (!workspaceId)
|
|
175
|
+
return 0;
|
|
176
|
+
const row = this.db.prepare(`
|
|
177
|
+
SELECT COALESCE(SUM(size_bytes), 0) AS total_bytes
|
|
178
|
+
FROM workspace_assets
|
|
179
|
+
WHERE tenant_id = ?
|
|
180
|
+
AND workspace_id = ?
|
|
181
|
+
AND deleted_at IS NULL
|
|
182
|
+
`).get(this.tenantId, workspaceId);
|
|
183
|
+
const total = Number(row?.total_bytes || 0);
|
|
184
|
+
return Number.isFinite(total) ? Math.max(0, Math.floor(total)) : 0;
|
|
185
|
+
}
|
|
162
186
|
resolveCanonicalTransitionSource(updatedBy) {
|
|
163
187
|
if (updatedBy === 'registration'
|
|
164
188
|
|| updatedBy === 'checkout-start'
|
|
@@ -442,7 +466,8 @@ export class PlanEntitlementService {
|
|
|
442
466
|
entitlementState: nextState,
|
|
443
467
|
planId: input.billingMirrorPlanId !== undefined
|
|
444
468
|
? input.billingMirrorPlanId
|
|
445
|
-
: (nextState === 'pending_plan_selection' ? null : planVersion.planId)
|
|
469
|
+
: (nextState === 'pending_plan_selection' ? null : planVersion.planId),
|
|
470
|
+
effectiveUntil: input.effectiveUntil || null
|
|
446
471
|
});
|
|
447
472
|
return this.getAccountEntitlement(accountId);
|
|
448
473
|
}
|
|
@@ -532,6 +557,32 @@ export class PlanEntitlementService {
|
|
|
532
557
|
}
|
|
533
558
|
return { limit: null, feature };
|
|
534
559
|
}
|
|
560
|
+
resolveAiProfileLimitForWorkspace(workspaceId) {
|
|
561
|
+
const normalizedWorkspaceId = String(workspaceId || '').trim();
|
|
562
|
+
if (!normalizedWorkspaceId) {
|
|
563
|
+
return { limit: null, feature: null };
|
|
564
|
+
}
|
|
565
|
+
const accountId = this.deps.getWorkspaceOwnerUserId(normalizedWorkspaceId);
|
|
566
|
+
if (!accountId) {
|
|
567
|
+
return { limit: null, feature: null };
|
|
568
|
+
}
|
|
569
|
+
let entitlement;
|
|
570
|
+
try {
|
|
571
|
+
entitlement = this.getAccountEntitlement(accountId);
|
|
572
|
+
}
|
|
573
|
+
catch (error) {
|
|
574
|
+
if (error instanceof TaskforceRuleError && error.code === 'ENTITLEMENT_NOT_FOUND') {
|
|
575
|
+
return { limit: null, feature: null };
|
|
576
|
+
}
|
|
577
|
+
throw error;
|
|
578
|
+
}
|
|
579
|
+
const feature = entitlement.version.features[AI_PROFILES_FEATURE_KEY] || null;
|
|
580
|
+
if (!feature || feature.access !== 'limited') {
|
|
581
|
+
return { limit: null, feature };
|
|
582
|
+
}
|
|
583
|
+
const configuredLimit = this.readPositiveIntegerConfig(feature.config?.[AI_PROFILES_LIMIT_CONFIG_KEY], 1);
|
|
584
|
+
return { limit: configuredLimit ?? 1, feature };
|
|
585
|
+
}
|
|
535
586
|
planHasAnyActivePricing(planIdRaw) {
|
|
536
587
|
const planId = String(planIdRaw || '').trim().toLowerCase();
|
|
537
588
|
if (!planId)
|
|
@@ -632,13 +683,19 @@ export class PlanEntitlementService {
|
|
|
632
683
|
const policy = billing.onboardingPolicy && typeof billing.onboardingPolicy === 'object'
|
|
633
684
|
? billing.onboardingPolicy
|
|
634
685
|
: null;
|
|
686
|
+
if (policy?.mode === 'require_plan_selection'
|
|
687
|
+
|| policy?.mode === 'auto_grant_free'
|
|
688
|
+
|| policy?.mode === 'auto_start_trial') {
|
|
689
|
+
return {
|
|
690
|
+
mode: policy.mode,
|
|
691
|
+
planVersionId: typeof policy.planVersionId === 'string'
|
|
692
|
+
? (policy.planVersionId.trim() || null)
|
|
693
|
+
: null
|
|
694
|
+
};
|
|
695
|
+
}
|
|
635
696
|
return {
|
|
636
|
-
mode:
|
|
637
|
-
|
|
638
|
-
: 'require_plan_selection',
|
|
639
|
-
planVersionId: typeof policy?.planVersionId === 'string'
|
|
640
|
-
? (policy.planVersionId.trim() || null)
|
|
641
|
-
: null
|
|
697
|
+
mode: 'require_plan_selection',
|
|
698
|
+
planVersionId: null
|
|
642
699
|
};
|
|
643
700
|
}
|
|
644
701
|
validateOnboardingPolicy(input) {
|
|
@@ -713,7 +770,12 @@ export class PlanEntitlementService {
|
|
|
713
770
|
}
|
|
714
771
|
resolveRegistrationOnboardingOutcome(selectedPlanVersionIdRaw) {
|
|
715
772
|
const selectedPlanVersionId = String(selectedPlanVersionIdRaw || '').trim();
|
|
716
|
-
|
|
773
|
+
const onboardingPolicy = this.getOnboardingPolicy();
|
|
774
|
+
const validation = this.validateOnboardingPolicy(onboardingPolicy);
|
|
775
|
+
if (!validation.valid) {
|
|
776
|
+
throw new TaskforceRuleError(validation.message || 'Onboarding policy is misconfigured.', 500, validation.code || 'ONBOARDING_POLICY_INVALID', validation.details);
|
|
777
|
+
}
|
|
778
|
+
if (onboardingPolicy.mode === 'require_plan_selection' && selectedPlanVersionId) {
|
|
717
779
|
const planVersion = this.getPlanVersionSnapshot(selectedPlanVersionId);
|
|
718
780
|
const commercialState = this.planVersionHasActiveFreePricing(planVersion.planVersionId)
|
|
719
781
|
? 'active'
|
|
@@ -726,11 +788,6 @@ export class PlanEntitlementService {
|
|
|
726
788
|
planId: planVersion.planId
|
|
727
789
|
};
|
|
728
790
|
}
|
|
729
|
-
const onboardingPolicy = this.getOnboardingPolicy();
|
|
730
|
-
const validation = this.validateOnboardingPolicy(onboardingPolicy);
|
|
731
|
-
if (!validation.valid) {
|
|
732
|
-
throw new TaskforceRuleError(validation.message || 'Onboarding policy is misconfigured.', 500, validation.code || 'ONBOARDING_POLICY_INVALID', validation.details);
|
|
733
|
-
}
|
|
734
791
|
if (onboardingPolicy.mode === 'require_plan_selection') {
|
|
735
792
|
return {
|
|
736
793
|
requiresPlanSelection: true,
|
|
@@ -761,14 +818,22 @@ export class PlanEntitlementService {
|
|
|
761
818
|
const planId = typeof options?.planId === 'string'
|
|
762
819
|
? options.planId.trim().toLowerCase()
|
|
763
820
|
: '';
|
|
821
|
+
const shouldUpdateEffectiveUntil = options ? Object.prototype.hasOwnProperty.call(options, 'effectiveUntil') : false;
|
|
822
|
+
const effectiveUntil = shouldUpdateEffectiveUntil
|
|
823
|
+
? (typeof options?.effectiveUntil === 'string' ? options.effectiveUntil : null)
|
|
824
|
+
: null;
|
|
764
825
|
this.db.prepare(`
|
|
765
|
-
INSERT INTO user_billing (user_id, tenant_id, entitlement_state, tier, created_at, updated_at)
|
|
766
|
-
VALUES (?, ?, ?, ?, ?, ?)
|
|
826
|
+
INSERT INTO user_billing (user_id, tenant_id, entitlement_state, tier, effective_until, created_at, updated_at)
|
|
827
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
767
828
|
ON CONFLICT (user_id) DO UPDATE SET
|
|
768
829
|
entitlement_state = excluded.entitlement_state,
|
|
769
830
|
tier = excluded.tier,
|
|
831
|
+
effective_until = CASE
|
|
832
|
+
WHEN ? THEN excluded.effective_until
|
|
833
|
+
ELSE user_billing.effective_until
|
|
834
|
+
END,
|
|
770
835
|
updated_at = excluded.updated_at
|
|
771
|
-
`).run(userId, this.tenantId, entitlementState, planId, now, now);
|
|
836
|
+
`).run(userId, this.tenantId, entitlementState, planId, effectiveUntil, now, now, shouldUpdateEffectiveUntil ? 1 : 0);
|
|
772
837
|
}
|
|
773
838
|
getUserEntitlement(userId) {
|
|
774
839
|
const account = this.getAccountEntitlement(userId);
|
|
@@ -794,6 +859,34 @@ export class PlanEntitlementService {
|
|
|
794
859
|
throw error;
|
|
795
860
|
}
|
|
796
861
|
}
|
|
862
|
+
tryAutoHealLegacyPersonalFreeEntitlement(accountId, entitlement) {
|
|
863
|
+
if (entitlement.state !== 'canceled')
|
|
864
|
+
return null;
|
|
865
|
+
if (this.isTeamPlanVersion(entitlement.version))
|
|
866
|
+
return null;
|
|
867
|
+
const defaultPersonalVersion = this.tryGetDefaultPlanVersionByMode('personal');
|
|
868
|
+
if (!defaultPersonalVersion)
|
|
869
|
+
return null;
|
|
870
|
+
if (!this.planVersionHasActiveFreePricing(defaultPersonalVersion.planVersionId))
|
|
871
|
+
return null;
|
|
872
|
+
if (String(entitlement.planId || '').trim().toLowerCase() !== defaultPersonalVersion.planId)
|
|
873
|
+
return null;
|
|
874
|
+
if (entitlement.planVersionId === defaultPersonalVersion.planVersionId)
|
|
875
|
+
return null;
|
|
876
|
+
if (entitlement.version.versionNumber >= defaultPersonalVersion.versionNumber)
|
|
877
|
+
return null;
|
|
878
|
+
return this.transitionCanonicalCommercialState({
|
|
879
|
+
accountId,
|
|
880
|
+
planVersionId: defaultPersonalVersion.planVersionId,
|
|
881
|
+
state: 'active',
|
|
882
|
+
effectiveAt: entitlement.effectiveAt,
|
|
883
|
+
effectiveUntil: null,
|
|
884
|
+
billingMirrorPlanId: defaultPersonalVersion.planId,
|
|
885
|
+
source: 'system',
|
|
886
|
+
updatedBy: 'legacy-free-auto-heal',
|
|
887
|
+
createdAt: entitlement.effectiveAt
|
|
888
|
+
});
|
|
889
|
+
}
|
|
797
890
|
getAccountAccessStatus(accountId) {
|
|
798
891
|
const normalizedAccountId = String(accountId || '').trim();
|
|
799
892
|
if (!normalizedAccountId) {
|
|
@@ -801,7 +894,10 @@ export class PlanEntitlementService {
|
|
|
801
894
|
}
|
|
802
895
|
const onboardingPolicy = this.getOnboardingPolicy();
|
|
803
896
|
const validation = this.validateOnboardingPolicy(onboardingPolicy);
|
|
804
|
-
const
|
|
897
|
+
const currentEntitlement = this.tryGetAccountEntitlement(normalizedAccountId);
|
|
898
|
+
const entitlement = currentEntitlement
|
|
899
|
+
? (this.tryAutoHealLegacyPersonalFreeEntitlement(normalizedAccountId, currentEntitlement) || currentEntitlement)
|
|
900
|
+
: null;
|
|
805
901
|
if (!entitlement) {
|
|
806
902
|
return !validation.valid
|
|
807
903
|
? this.buildBlockedAccessStatus({
|
|
@@ -939,6 +1035,22 @@ export class PlanEntitlementService {
|
|
|
939
1035
|
const remaining = limit === null ? null : Math.max(0, limit - used);
|
|
940
1036
|
return { used, limit, remaining };
|
|
941
1037
|
}
|
|
1038
|
+
countAiProfileUsage(workspaceId) {
|
|
1039
|
+
const normalizedWorkspaceId = String(workspaceId || '').trim();
|
|
1040
|
+
if (!normalizedWorkspaceId) {
|
|
1041
|
+
throw new TaskforceRuleError('workspaceId is required', 400, 'WORKSPACE_ID_REQUIRED');
|
|
1042
|
+
}
|
|
1043
|
+
const row = this.db.prepare(`
|
|
1044
|
+
SELECT COUNT(*) AS c
|
|
1045
|
+
FROM ai_profiles
|
|
1046
|
+
WHERE tenant_id = ?
|
|
1047
|
+
AND workspace_id = ?
|
|
1048
|
+
`).get(this.tenantId, normalizedWorkspaceId);
|
|
1049
|
+
const used = Math.max(0, Number(row?.c || 0));
|
|
1050
|
+
const { limit } = this.resolveAiProfileLimitForWorkspace(normalizedWorkspaceId);
|
|
1051
|
+
const remaining = limit === null ? null : Math.max(0, limit - used);
|
|
1052
|
+
return { used, limit, remaining };
|
|
1053
|
+
}
|
|
942
1054
|
assertSeatAvailable(workspaceId) {
|
|
943
1055
|
const seatUsage = this.countWorkspaceSeatUsage(workspaceId, { includeInvited: true });
|
|
944
1056
|
if (seatUsage.limit !== null && seatUsage.used >= seatUsage.limit) {
|
|
@@ -976,6 +1088,19 @@ export class PlanEntitlementService {
|
|
|
976
1088
|
}
|
|
977
1089
|
return { allowed: true, reasonCode: 'OK', state: entitlement.state, seatUsage, feature };
|
|
978
1090
|
}
|
|
1091
|
+
if (featureKey === DOCUMENT_CLOUD_STORAGE_FEATURE_KEY && feature.access === 'limited') {
|
|
1092
|
+
const configuredLimitMb = this.readPositiveIntegerConfig(feature.config?.[DOCUMENT_CLOUD_STORAGE_LIMIT_CONFIG_KEY], 1) ?? 1;
|
|
1093
|
+
const storageLimitBytes = configuredLimitMb * 1024 * 1024;
|
|
1094
|
+
const rawRequestedBytes = input.context?.requestedBytes ?? input.context?.sizeBytes ?? input.context?.pendingSizeBytes;
|
|
1095
|
+
const requestedBytesNumber = Number(rawRequestedBytes);
|
|
1096
|
+
const requestedBytes = Number.isFinite(requestedBytesNumber)
|
|
1097
|
+
? Math.max(0, Math.floor(requestedBytesNumber))
|
|
1098
|
+
: 0;
|
|
1099
|
+
const usedBytes = this.countWorkspaceStorageUsageBytes(workspaceId);
|
|
1100
|
+
if ((usedBytes + requestedBytes) > storageLimitBytes) {
|
|
1101
|
+
return { allowed: false, reasonCode: 'UPLOAD_STORAGE_LIMIT_REACHED', state: entitlement.state, feature };
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
979
1104
|
return { allowed: true, reasonCode: 'OK', state: entitlement.state, feature };
|
|
980
1105
|
}
|
|
981
1106
|
getUserAccountPlanMode(userId) {
|
|
@@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest';
|
|
|
2
2
|
import fs from 'fs';
|
|
3
3
|
import os from 'os';
|
|
4
4
|
import path from 'path';
|
|
5
|
-
import { TaskforceCore
|
|
5
|
+
import { TaskforceCore } from './Taskforce';
|
|
6
6
|
function makeProjectRoot() {
|
|
7
7
|
return fs.mkdtempSync(path.join(os.tmpdir(), 'taskforce-plan-version-policy-'));
|
|
8
8
|
}
|
|
@@ -11,8 +11,8 @@ function openCore(projectRoot) {
|
|
|
11
11
|
projectRoot,
|
|
12
12
|
storagePath: path.join(projectRoot, '.taskforce')
|
|
13
13
|
});
|
|
14
|
-
core.upsertPlanCatalog({ planId: '
|
|
15
|
-
core.upsertPlanCatalog({ planId: '
|
|
14
|
+
core.upsertPlanCatalog({ planId: 'free', displayName: 'Free', status: 'archived' });
|
|
15
|
+
core.upsertPlanCatalog({ planId: 'team-5', displayName: 'Team 5', status: 'archived' });
|
|
16
16
|
return core;
|
|
17
17
|
}
|
|
18
18
|
describe('TaskforceCore plan version policy', () => {
|
|
@@ -20,17 +20,17 @@ describe('TaskforceCore plan version policy', () => {
|
|
|
20
20
|
const projectRoot = makeProjectRoot();
|
|
21
21
|
const core = openCore(projectRoot);
|
|
22
22
|
try {
|
|
23
|
-
core.createPlanVersion({ planId: '
|
|
23
|
+
core.createPlanVersion({ planId: 'free', versionNumber: 2, isDefault: false });
|
|
24
24
|
const db = core.db;
|
|
25
25
|
db.prepare(`
|
|
26
26
|
UPDATE plan_versions
|
|
27
27
|
SET is_default = 0
|
|
28
28
|
WHERE tenant_id = ? AND plan_id = ?
|
|
29
|
-
`).run('default', '
|
|
30
|
-
const versions = core.listPlanVersions('
|
|
29
|
+
`).run('default', 'free');
|
|
30
|
+
const versions = core.listPlanVersions('free');
|
|
31
31
|
const defaults = versions.filter((version) => version.isDefault);
|
|
32
32
|
expect(defaults).toHaveLength(1);
|
|
33
|
-
expect(defaults[0].planVersionId).toBe('
|
|
33
|
+
expect(defaults[0].planVersionId).toBe('free-v2');
|
|
34
34
|
}
|
|
35
35
|
finally {
|
|
36
36
|
core.close();
|
|
@@ -43,11 +43,11 @@ describe('TaskforceCore plan version policy', () => {
|
|
|
43
43
|
try {
|
|
44
44
|
expect(() => {
|
|
45
45
|
core.updatePlanVersion({
|
|
46
|
-
planId: '
|
|
46
|
+
planId: 'free',
|
|
47
47
|
versionNumber: 1,
|
|
48
48
|
isDefault: false
|
|
49
49
|
});
|
|
50
|
-
}).toThrowError(
|
|
50
|
+
}).toThrowError(/exactly one default version/i);
|
|
51
51
|
}
|
|
52
52
|
finally {
|
|
53
53
|
core.close();
|
|
@@ -59,13 +59,13 @@ describe('TaskforceCore plan version policy', () => {
|
|
|
59
59
|
const core = openCore(projectRoot);
|
|
60
60
|
try {
|
|
61
61
|
core.createPlanVersion({
|
|
62
|
-
planId: '
|
|
62
|
+
planId: 'team-5',
|
|
63
63
|
versionNumber: 2,
|
|
64
64
|
isDefault: true,
|
|
65
65
|
seatLimit: 3
|
|
66
66
|
});
|
|
67
67
|
core.updatePlanVersionFeatures({
|
|
68
|
-
planVersionId: '
|
|
68
|
+
planVersionId: 'team-5-v2',
|
|
69
69
|
features: [
|
|
70
70
|
{
|
|
71
71
|
featureKey: 'collaboration.seat_limit',
|
|
@@ -74,7 +74,7 @@ describe('TaskforceCore plan version policy', () => {
|
|
|
74
74
|
}
|
|
75
75
|
]
|
|
76
76
|
});
|
|
77
|
-
const version = core.getPlanVersionFeatures('
|
|
77
|
+
const version = core.getPlanVersionFeatures('team-5', 'team-5-v2');
|
|
78
78
|
expect(version.seatLimit).toBe(3);
|
|
79
79
|
expect(version.features['collaboration.seat_limit']?.config || {}).toEqual({});
|
|
80
80
|
}
|
|
@@ -109,19 +109,19 @@ describe('TaskforceCore plan version policy', () => {
|
|
|
109
109
|
const core = openCore(projectRoot);
|
|
110
110
|
try {
|
|
111
111
|
core.updatePlanVersion({
|
|
112
|
-
planId: '
|
|
112
|
+
planId: 'team-5',
|
|
113
113
|
versionNumber: 1,
|
|
114
114
|
seatLimit: 5
|
|
115
115
|
});
|
|
116
116
|
core.updatePlanVersionFeatures({
|
|
117
|
-
planVersionId: '
|
|
117
|
+
planVersionId: 'team-5-v1',
|
|
118
118
|
features: [
|
|
119
119
|
{ featureKey: 'collaboration.invites', access: 'enabled', config: {} },
|
|
120
120
|
{ featureKey: 'collaboration.team_management', access: 'enabled', config: {} }
|
|
121
121
|
]
|
|
122
122
|
});
|
|
123
123
|
const version = core.updatePlanVersionFeatures({
|
|
124
|
-
planVersionId: '
|
|
124
|
+
planVersionId: 'team-5-v1',
|
|
125
125
|
features: [
|
|
126
126
|
{ featureKey: 'collaboration.team_management', access: 'disabled', config: {} }
|
|
127
127
|
]
|
|
@@ -139,7 +139,7 @@ describe('TaskforceCore plan version policy', () => {
|
|
|
139
139
|
const core = openCore(projectRoot);
|
|
140
140
|
try {
|
|
141
141
|
const version = core.updatePlanVersionFeatures({
|
|
142
|
-
planVersionId: '
|
|
142
|
+
planVersionId: 'free-v1',
|
|
143
143
|
features: [
|
|
144
144
|
{ featureKey: 'workspace.switching', access: 'limited', config: { maxWorkspaces: 0 } },
|
|
145
145
|
{ featureKey: 'context.uploads', access: 'limited', config: { storageLimitMb: -50 } }
|
|
@@ -153,22 +153,15 @@ describe('TaskforceCore plan version policy', () => {
|
|
|
153
153
|
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
154
154
|
}
|
|
155
155
|
});
|
|
156
|
-
it('starts new plans disabled
|
|
156
|
+
it('starts new plans disabled before pricing is configured', () => {
|
|
157
157
|
const projectRoot = makeProjectRoot();
|
|
158
158
|
const core = openCore(projectRoot);
|
|
159
159
|
try {
|
|
160
160
|
const created = core.upsertPlanCatalog({
|
|
161
|
-
planId: '
|
|
162
|
-
displayName: '
|
|
161
|
+
planId: 'agency',
|
|
162
|
+
displayName: 'Agency'
|
|
163
163
|
});
|
|
164
164
|
expect(created.status).toBe('archived');
|
|
165
|
-
expect(() => {
|
|
166
|
-
core.upsertPlanCatalog({
|
|
167
|
-
planId: 'enterprise',
|
|
168
|
-
displayName: 'Enterprise',
|
|
169
|
-
status: 'active'
|
|
170
|
-
});
|
|
171
|
-
}).toThrowError(TaskforceRuleError);
|
|
172
165
|
}
|
|
173
166
|
finally {
|
|
174
167
|
core.close();
|