@taskforcehq/taskforce 0.3.305 → 0.3.306
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/dist/Taskforce.module.css +295 -89
- package/dist/TaskforceCore.js +215 -43
- package/dist/TaskforceCore.test.js +433 -28
- package/dist/assets/fonts/CourierPrime-Bold.woff2 +0 -0
- package/dist/assets/fonts/CourierPrime-BoldItalic.woff2 +0 -0
- package/dist/assets/fonts/CourierPrime-Italic.woff2 +0 -0
- package/dist/assets/fonts/CourierPrime-Regular.woff2 +0 -0
- package/dist/assets/fonts/Noir_medium.woff2 +0 -0
- package/dist/assets/fonts/Noir_regular.woff2 +0 -0
- package/dist/assets/fonts/RussoOne-Regular.woff2 +0 -0
- package/dist/assets/fonts/SpecialElite-Regular.woff2 +0 -0
- package/dist/compat/workspaceSyncCompat.js +46 -2
- package/dist/compat/workspaceSyncCompat.test.js +38 -1
- package/dist/components/context/ContextAttachmentManager.d.ts +14 -0
- package/dist/components/context/ContextAttachmentManager.js +549 -0
- package/dist/components/features/AgentsModule.d.ts +10 -1
- package/dist/components/features/AgentsModule.js +260 -15
- package/dist/components/features/AgentsModule.test.js +255 -5
- package/dist/components/features/DocumentViewer.d.ts +5 -1
- package/dist/components/features/DocumentViewer.js +60 -3
- package/dist/components/features/DocumentWorkspace.js +1 -1
- package/dist/components/features/DocumentWorkspace.test.js +33 -0
- package/dist/components/features/documentWorkspaceModel.d.ts +1 -1
- package/dist/components/task/TaskCard.js +19 -28
- package/dist/components/task/TaskCard.test.js +38 -3
- package/dist/components/task/TaskContextUpload.js +4 -526
- package/dist/components/task/TaskForm.js +85 -90
- package/dist/components/task/TaskForm.test.js +110 -8
- package/dist/components/task/TaskKanban.d.ts +2 -1
- package/dist/components/task/TaskKanban.js +94 -13
- package/dist/components/task/TaskKanban.test.js +36 -0
- package/dist/components/ui/EditableAvatarButton.d.ts +18 -0
- package/dist/components/ui/EditableAvatarButton.js +18 -0
- package/dist/components/views/PlanComparisonPage.d.ts +9 -1
- package/dist/components/views/PlanComparisonPage.js +70 -14
- package/dist/components/views/PlanComparisonPage.test.js +238 -0
- package/dist/components/views/PlansPage.js +127 -36
- package/dist/components/views/PlansPage.test.js +294 -12
- package/dist/components/views/StandaloneLayout.js +283 -533
- package/dist/components/views/panels/PlanningDrawer.d.ts +8 -1
- package/dist/components/views/panels/PlanningDrawer.js +18 -11
- package/dist/components/views/panels/PlanningDrawer.test.d.ts +1 -0
- package/dist/components/views/panels/PlanningDrawer.test.js +141 -0
- package/dist/components/views/planningScope.d.ts +20 -0
- package/dist/components/views/planningScope.js +25 -0
- package/dist/components/views/planningScope.test.d.ts +1 -0
- package/dist/components/views/planningScope.test.js +60 -0
- package/dist/components/views/standalone/modals/AccountHubModal.d.ts +26 -1
- package/dist/components/views/standalone/modals/AccountHubModal.js +76 -2
- package/dist/components/views/standalone/modals/AccountHubModal.test.js +20 -3
- package/dist/components/views/standalone/modals/AvatarImageManagerModal.d.ts +20 -0
- package/dist/components/views/standalone/modals/AvatarImageManagerModal.js +127 -0
- package/dist/components/views/standalone/modals/EditProfileModal.d.ts +2 -9
- package/dist/components/views/standalone/modals/EditProfileModal.js +3 -2
- package/dist/components/views/standalone/modals/SyncEnableWarningModal.d.ts +9 -0
- package/dist/components/views/standalone/modals/SyncEnableWarningModal.js +6 -0
- package/dist/components/views/standalone/modals/SyncEnableWarningModal.test.d.ts +1 -0
- package/dist/components/views/standalone/modals/SyncEnableWarningModal.test.js +24 -0
- package/dist/components/views/standalone/modals/SyncStatusModal.d.ts +2 -1
- package/dist/components/views/standalone/modals/SyncStatusModal.js +2 -2
- package/dist/core/AiProfileService.d.ts +39 -2
- package/dist/core/AiProfileService.js +639 -73
- package/dist/core/AiProfileService.test.js +19 -1
- package/dist/core/AiProfiles.test.js +566 -7
- package/dist/core/AttachmentLinkService.js +57 -15
- package/dist/core/AuthIdentityService.d.ts +107 -0
- package/dist/core/AuthIdentityService.js +284 -0
- package/dist/core/AuthTokenService.d.ts +4 -0
- package/dist/core/AuthTokenService.js +34 -6
- package/dist/core/AuthTokenService.test.js +3 -3
- package/dist/core/EntitlementsPolicy.test.js +2 -2
- package/dist/core/GlobalSettingsService.js +115 -6
- package/dist/core/PlanEntitlementService.d.ts +23 -2
- package/dist/core/PlanEntitlementService.js +343 -57
- package/dist/core/PlanFeatureCatalog.test.d.ts +1 -0
- package/dist/core/PlanFeatureCatalog.test.js +84 -0
- package/dist/core/PlanVersionPolicy.test.js +35 -2
- package/dist/core/PlanningEntities.test.js +52 -0
- package/dist/core/SignupMonetizationSettings.test.js +199 -27
- package/dist/core/SyncReconciliationService.js +2 -65
- package/dist/core/SystemAdmin.test.js +42 -42
- package/dist/core/TaskAttachmentsCanonical.test.js +51 -1
- package/dist/core/Taskforce.d.ts +225 -14
- package/dist/core/Taskforce.js +765 -192
- package/dist/core/Taskforce.listTasksSlim.test.d.ts +1 -0
- package/dist/core/Taskforce.listTasksSlim.test.js +91 -0
- package/dist/core/UserProfileAvatarDrafts.test.js +30 -3
- package/dist/core/WorkspaceLifecycleService.js +0 -5
- package/dist/core/WorkspacePermissions.test.js +25 -1
- package/dist/core/WorkspacePlanMode.test.js +117 -16
- package/dist/core/shared.d.ts +1 -1
- package/dist/core/shared.js +5 -1
- package/dist/core/types.d.ts +68 -2
- package/dist/hooks/auth/useTaskforceAuthBootstrap.js +26 -1
- package/dist/hooks/sync/bootstrap.js +2 -0
- package/dist/hooks/sync/controlPlane.d.ts +37 -0
- package/dist/hooks/sync/controlPlane.js +78 -0
- package/dist/hooks/sync/controlPlane.test.d.ts +1 -0
- package/dist/hooks/sync/controlPlane.test.js +121 -0
- package/dist/hooks/sync/lifecyclePolicy.d.ts +45 -0
- package/dist/hooks/sync/lifecyclePolicy.js +93 -0
- package/dist/hooks/sync/lifecyclePolicy.test.d.ts +1 -0
- package/dist/hooks/sync/lifecyclePolicy.test.js +124 -0
- package/dist/hooks/sync/orchestratorShared.d.ts +10 -1
- package/dist/hooks/sync/orchestratorShared.js +68 -47
- package/dist/hooks/sync/orchestratorShared.test.js +50 -1
- package/dist/hooks/sync/pullLifecycle.d.ts +31 -0
- package/dist/hooks/sync/pullLifecycle.js +24 -0
- package/dist/hooks/sync/pullLifecycle.test.d.ts +1 -0
- package/dist/hooks/sync/pullLifecycle.test.js +80 -0
- package/dist/hooks/sync/recovery.d.ts +16 -2
- package/dist/hooks/sync/recovery.js +171 -53
- package/dist/hooks/sync/recovery.test.d.ts +1 -0
- package/dist/hooks/sync/recovery.test.js +292 -0
- package/dist/hooks/sync/transfers.d.ts +15 -3
- package/dist/hooks/sync/transfers.js +179 -62
- package/dist/hooks/sync/transfers.test.d.ts +1 -0
- package/dist/hooks/sync/transfers.test.js +396 -0
- package/dist/hooks/sync/useRecentSyncEvents.d.ts +21 -0
- package/dist/hooks/sync/useRecentSyncEvents.js +92 -0
- package/dist/hooks/sync/useRecentSyncEvents.test.d.ts +1 -0
- package/dist/hooks/sync/useRecentSyncEvents.test.js +124 -0
- package/dist/hooks/sync/useSyncStatusActions.d.ts +30 -0
- package/dist/hooks/sync/useSyncStatusActions.js +88 -0
- package/dist/hooks/sync/useSyncStatusActions.test.d.ts +1 -0
- package/dist/hooks/sync/useSyncStatusActions.test.js +133 -0
- package/dist/hooks/sync/useSyncStatusControls.d.ts +25 -0
- package/dist/hooks/sync/useSyncStatusControls.js +60 -0
- package/dist/hooks/sync/useSyncStatusControls.test.d.ts +1 -0
- package/dist/hooks/sync/useSyncStatusControls.test.js +88 -0
- package/dist/hooks/useSyncOrchestrator.aiProfiles.test.js +12 -0
- package/dist/hooks/useSyncOrchestrator.d.ts +9 -2
- package/dist/hooks/useSyncOrchestrator.js +359 -178
- package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +407 -0
- package/dist/hooks/useTaskData.js +8 -3
- package/dist/hooks/useTaskData.test.js +45 -0
- package/dist/hooks/useTaskforce.d.ts +60 -2
- package/dist/hooks/useTaskforce.js +549 -240
- package/dist/hooks/useTaskforce.runtime-routing.test.d.ts +1 -0
- package/dist/hooks/useTaskforce.runtime-routing.test.js +152 -0
- package/dist/hooks/useTaskforce.sync-behavior.test.js +2380 -212
- package/dist/hooks/useWorkspaceSyncController.d.ts +1 -0
- package/dist/hooks/useWorkspaceSyncController.js +6 -1
- package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.d.ts +16 -0
- package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.js +316 -35
- package/dist/localization/locales/en-US.d.ts +0 -1
- package/dist/localization/locales/en-US.js +0 -1
- package/dist/localization/locales/es-419.js +0 -1
- package/dist/localization/locales/pt-BR.js +0 -1
- package/dist/mcp/documentAssetRegistrar.js +3 -3
- package/dist/mcp/runtime.d.ts +2 -0
- package/dist/mcp/runtime.js +131 -64
- package/dist/mcp/runtime.test.js +291 -12
- package/dist/migrations/taskSchemaMigrations.d.ts +1 -1
- package/dist/migrations/taskSchemaMigrations.js +171 -61
- package/dist/migrations/taskSchemaMigrations.test.js +15 -0
- package/dist/resources/templates/workflow-sources/workflowDocs.mjs +6 -6
- package/dist/resources/templates/workflows/collaborate.yaml +1 -1
- package/dist/resources/templates/workflows/evaluate.yaml +2 -2
- package/dist/resources/templates/workflows/plan.yaml +1 -1
- package/dist/resources/templates/workflows/review.yaml +2 -2
- package/dist/server/annotatedAttachmentsRoutes.test.js +3 -3
- package/dist/server/auth/providers/apple.d.ts +33 -0
- package/dist/server/auth/providers/apple.js +82 -0
- package/dist/server/auth/providers/appleClientSecret.d.ts +37 -0
- package/dist/server/auth/providers/appleClientSecret.js +65 -0
- package/dist/server/auth/providers/github.d.ts +26 -0
- package/dist/server/auth/providers/github.js +86 -0
- package/dist/server/auth/providers/google.d.ts +21 -0
- package/dist/server/auth/providers/google.js +56 -0
- package/dist/server/auth/providers/types.d.ts +21 -0
- package/dist/server/auth/providers/types.js +1 -0
- package/dist/server/auth.d.ts +2 -0
- package/dist/server/auth.js +6 -3
- package/dist/server/documentReviewRoutes.test.js +36 -3
- package/dist/server/index.cookieProxy.test.js +1 -0
- package/dist/server/index.d.ts +12 -0
- package/dist/server/index.js +120 -9
- package/dist/server/index.rateLimit.test.js +3 -0
- package/dist/server/index.test.js +106 -3
- package/dist/server/routes/admin.d.ts +3 -0
- package/dist/server/routes/admin.js +272 -19
- package/dist/server/routes/auth.d.ts +15 -2
- package/dist/server/routes/auth.js +994 -57
- package/dist/server/routes/authSupport.d.ts +1 -1
- package/dist/server/routes/authSupport.js +1 -1
- package/dist/server/routes/billing.js +361 -35
- package/dist/server/routes/billing.test.js +592 -12
- package/dist/server/routes/documents.d.ts +1 -0
- package/dist/server/routes/documents.js +172 -70
- package/dist/server/routes/shared.d.ts +28 -2
- package/dist/server/routes/shared.js +69 -4
- package/dist/server/routes/sync.integration.test.js +437 -39
- package/dist/server/routes/sync.js +61 -605
- package/dist/server/routes/syncAuxRoutes.d.ts +15 -0
- package/dist/server/routes/syncAuxRoutes.js +91 -0
- package/dist/server/routes/syncAuxRoutes.test.d.ts +1 -0
- package/dist/server/routes/syncAuxRoutes.test.js +158 -0
- package/dist/server/routes/syncPullApplyRoutes.d.ts +36 -0
- package/dist/server/routes/syncPullApplyRoutes.js +204 -0
- package/dist/server/routes/syncPushRoutes.d.ts +24 -0
- package/dist/server/routes/syncPushRoutes.js +212 -0
- package/dist/server/routes/syncRouteGuards.d.ts +36 -0
- package/dist/server/routes/syncRouteGuards.js +67 -0
- package/dist/server/routes/syncRouteGuards.test.d.ts +1 -0
- package/dist/server/routes/syncRouteGuards.test.js +94 -0
- package/dist/server/routes/syncRouteTypes.d.ts +13 -0
- package/dist/server/routes/syncRouteTypes.js +1 -0
- package/dist/server/routes/syncSnapshotRoutes.d.ts +66 -0
- package/dist/server/routes/syncSnapshotRoutes.js +180 -0
- package/dist/server/routes/tasks.js +43 -1
- package/dist/server/routes/workspaces.js +36 -5
- package/dist/server/routes.js +60 -3
- package/dist/server/routes.test.js +1824 -171
- package/dist/services/workflowExportSnapshots.test.js +2 -0
- package/dist/shared/aiProfileSeatScope.d.ts +5 -0
- package/dist/shared/aiProfileSeatScope.js +21 -0
- package/dist/shared/passwordPolicy.d.ts +13 -0
- package/dist/shared/passwordPolicy.js +84 -0
- package/dist/shared/passwordPolicy.test.d.ts +1 -0
- package/dist/shared/passwordPolicy.test.js +34 -0
- package/dist/storage/workspaceAssetStore.d.ts +15 -3
- package/dist/storage/workspaceAssetStore.js +132 -44
- package/dist/storage/workspaceAssetStore.test.js +93 -6
- package/dist/styles/fonts.css +70 -0
- package/dist/sync/collaborationSyncPayload.d.ts +6 -1
- package/dist/sync/collaborationSyncPayload.js +17 -11
- package/dist/sync/collaborationSyncPayload.test.js +13 -1
- package/dist/sync/contentSyncState.d.ts +4 -8
- package/dist/sync/contentSyncState.js +17 -70
- package/dist/sync/syncApplyHandlers.d.ts +7 -0
- package/dist/sync/syncApplyHandlers.js +28 -13
- package/dist/sync/syncApplyHandlers.test.js +70 -3
- package/dist/sync/syncFieldSemantics.d.ts +12 -0
- package/dist/sync/syncFieldSemantics.js +55 -0
- package/dist/sync/syncResourceAdapters.d.ts +44 -0
- package/dist/sync/syncResourceAdapters.js +77 -0
- package/dist/sync/syncService.d.ts +5 -0
- package/dist/sync/syncService.js +52 -0
- package/dist/sync/syncService.test.js +57 -3
- package/dist/sync/taskSyncChangeKey.d.ts +2 -0
- package/dist/sync/taskSyncChangeKey.js +143 -0
- package/dist/sync/taskSyncPayload.js +3 -8
- package/dist/sync/workspacePullFeed.js +4 -3
- package/dist/sync/workspacePullFeed.test.js +43 -0
- package/dist/sync/workspaceRepair.js +5 -2
- package/dist/sync/workspaceSyncModel.d.ts +45 -0
- package/dist/sync/workspaceSyncModel.js +183 -28
- package/dist/sync/workspaceSyncModel.test.js +291 -12
- package/dist/sync/workspaceSyncState.d.ts +17 -0
- package/dist/sync/workspaceSyncState.js +2 -1
- package/dist/sync/workspaceSyncSurface.js +3 -8
- package/dist/types.d.ts +17 -0
- package/dist/ui/assets/AgentsModule-DrLawwNl.js +1 -0
- package/dist/ui/assets/{AnnotatedAttachmentWorkspace-BG6P_GVW.js → AnnotatedAttachmentWorkspace-Z9_whf7F.js} +1 -1
- package/dist/ui/assets/CourierPrime-Bold-BuLj_dpw.woff2 +0 -0
- package/dist/ui/assets/CourierPrime-BoldItalic-BrK2pUkZ.woff2 +0 -0
- package/dist/ui/assets/CourierPrime-Italic-DFUZK5Ey.woff2 +0 -0
- package/dist/ui/assets/CourierPrime-Regular-BsKphzVf.woff2 +0 -0
- package/dist/ui/assets/DocumentWorkspace-B03MaSkw.js +252 -0
- package/dist/ui/assets/DocumentWorkspace-Dhdso07p.css +1 -0
- package/dist/ui/assets/{InitiativesModule-Dhm7M8e7.js → InitiativesModule-9E6ParrY.js} +1 -1
- package/dist/ui/assets/Noir_medium-bxQwKGzB.woff2 +0 -0
- package/dist/ui/assets/Noir_regular-ojf2kxlG.woff2 +0 -0
- package/dist/ui/assets/PlansPage-CvfnMiWq.css +1 -0
- package/dist/ui/assets/PlansPage-DRXscYxH.js +1 -0
- package/dist/ui/assets/RussoOne-Regular-Cu_qq_qC.woff2 +0 -0
- package/dist/ui/assets/SpecialElite-Regular-Di6gSvAS.woff2 +0 -0
- package/dist/ui/assets/{TaskSettings-ln0aF7xc.js → TaskSettings-DZX4jk7e.js} +1 -1
- package/dist/ui/assets/{WorkflowsModule-BcS4afRn.js → WorkflowsModule-BA7jcEpH.js} +1 -1
- package/dist/ui/assets/index-9eT8e0Lu.css +1 -0
- package/dist/ui/assets/index-C6k75jr8.js +6 -0
- package/dist/ui/assets/{vendor-icons-BSUaRwF8.js → vendor-icons-DuEd_65c.js} +1 -1
- package/dist/ui/fonts/CourierPrime-Bold.ttf +0 -0
- package/dist/ui/fonts/CourierPrime-Bold.woff2 +0 -0
- package/dist/ui/fonts/CourierPrime-BoldItalic.ttf +0 -0
- package/dist/ui/fonts/CourierPrime-BoldItalic.woff2 +0 -0
- package/dist/ui/fonts/CourierPrime-Italic.ttf +0 -0
- package/dist/ui/fonts/CourierPrime-Italic.woff2 +0 -0
- package/dist/ui/fonts/CourierPrime-Regular.ttf +0 -0
- package/dist/ui/fonts/CourierPrime-Regular.woff2 +0 -0
- package/dist/ui/fonts/Noir_medium.otf +0 -0
- package/dist/ui/fonts/Noir_medium.woff2 +0 -0
- package/dist/ui/fonts/Noir_regular.otf +0 -0
- package/dist/ui/fonts/Noir_regular.woff2 +0 -0
- package/dist/ui/fonts/RussoOne-Regular.woff2 +0 -0
- package/dist/ui/fonts/SpecialElite-Regular.ttf +0 -0
- package/dist/ui/fonts/SpecialElite-Regular.woff2 +0 -0
- package/dist/ui/index.html +3 -3
- package/dist/utils/accountProfileSummaryCache.d.ts +5 -0
- package/dist/utils/assignees.d.ts +2 -0
- package/dist/utils/assignees.js +2 -0
- package/dist/utils/avatarUpload.d.ts +27 -0
- package/dist/utils/avatarUpload.js +132 -0
- package/dist/utils/avatarUpload.test.d.ts +1 -0
- package/dist/utils/avatarUpload.test.js +40 -0
- package/dist/utils/bootstrapTrace.d.ts +7 -0
- package/dist/utils/bootstrapTrace.js +29 -0
- package/dist/utils/commercialLifecyclePresentation.js +3 -0
- package/dist/utils/contextAssetEvents.d.ts +2 -0
- package/dist/utils/syncEventDetails.d.ts +3 -0
- package/dist/utils/syncEventDetails.js +64 -0
- package/dist/utils/syncEventDetails.test.d.ts +1 -0
- package/dist/utils/syncEventDetails.test.js +23 -0
- package/dist/utils/syncEventPresentation.d.ts +15 -0
- package/dist/utils/syncEventPresentation.js +129 -0
- package/dist/utils/syncEventPresentation.test.d.ts +1 -0
- package/dist/utils/syncEventPresentation.test.js +64 -0
- package/dist/utils/syncStatusPresentation.d.ts +72 -0
- package/dist/utils/syncStatusPresentation.js +217 -0
- package/dist/utils/syncStatusPresentation.test.d.ts +1 -0
- package/dist/utils/syncStatusPresentation.test.js +164 -0
- package/dist/utils/taskActivity.d.ts +2 -0
- package/dist/utils/taskActivity.js +228 -22
- package/dist/utils/taskActivity.test.js +119 -8
- package/dist/utils/workspaceSyncPresentation.d.ts +4 -0
- package/dist/utils/workspaceSyncPresentation.js +68 -2
- package/dist/utils/workspaceSyncPresentation.test.js +36 -1
- package/package.json +3 -2
- package/scripts/export-sqlite-to-postgres.mjs +7 -9
- package/scripts/run-e2e-realtime.sh +17 -3
- package/scripts/run-smoke.sh +17 -5
- package/scripts/run-soak.sh +17 -5
- package/src/resources/templates/workflow-sources/workflowDocs.mjs +6 -6
- package/src/resources/templates/workflows/collaborate.yaml +1 -1
- package/src/resources/templates/workflows/evaluate.yaml +2 -2
- package/src/resources/templates/workflows/plan.yaml +1 -1
- package/src/resources/templates/workflows/review.yaml +2 -2
- package/dist/ui/assets/AgentsModule-N2MnQBZk.js +0 -1
- package/dist/ui/assets/DocumentWorkspace-B3nV_Gc5.css +0 -1
- package/dist/ui/assets/DocumentWorkspace-DRbDMwh8.js +0 -252
- package/dist/ui/assets/PlansPage-C2dd2n1X.css +0 -1
- package/dist/ui/assets/PlansPage-Cq0zXj3I.js +0 -1
- package/dist/ui/assets/RussoOne-Regular-C3BxZIj7.ttf +0 -0
- package/dist/ui/assets/index-C2-OXZV7.css +0 -1
- package/dist/ui/assets/index-DcSI5F86.js +0 -6
- package/scripts/migrate-planning-model.ts +0 -233
|
@@ -180,6 +180,16 @@ function setupBillingSchema(db) {
|
|
|
180
180
|
created_at TEXT NOT NULL,
|
|
181
181
|
updated_at TEXT NOT NULL
|
|
182
182
|
);
|
|
183
|
+
CREATE TABLE plan_feature_matrix (
|
|
184
|
+
tenant_id TEXT NOT NULL,
|
|
185
|
+
plan_version_id TEXT NOT NULL,
|
|
186
|
+
feature_key TEXT NOT NULL,
|
|
187
|
+
access TEXT NOT NULL DEFAULT 'disabled',
|
|
188
|
+
config_json TEXT NOT NULL DEFAULT '{}',
|
|
189
|
+
created_at TEXT NOT NULL,
|
|
190
|
+
updated_at TEXT NOT NULL,
|
|
191
|
+
PRIMARY KEY (tenant_id, plan_version_id, feature_key)
|
|
192
|
+
);
|
|
183
193
|
CREATE TABLE plan_catalog (
|
|
184
194
|
plan_id TEXT PRIMARY KEY,
|
|
185
195
|
tenant_id TEXT NOT NULL,
|
|
@@ -188,6 +198,16 @@ function setupBillingSchema(db) {
|
|
|
188
198
|
created_at TEXT NOT NULL,
|
|
189
199
|
updated_at TEXT NOT NULL
|
|
190
200
|
);
|
|
201
|
+
CREATE TABLE plan_feature_catalog_overrides (
|
|
202
|
+
tenant_id TEXT NOT NULL,
|
|
203
|
+
feature_key TEXT NOT NULL,
|
|
204
|
+
public_label TEXT,
|
|
205
|
+
public_description TEXT,
|
|
206
|
+
public_display_order INTEGER,
|
|
207
|
+
created_at TEXT NOT NULL,
|
|
208
|
+
updated_at TEXT NOT NULL,
|
|
209
|
+
PRIMARY KEY (tenant_id, feature_key)
|
|
210
|
+
);
|
|
191
211
|
CREATE TABLE account_entitlements (
|
|
192
212
|
account_id TEXT PRIMARY KEY,
|
|
193
213
|
tenant_id TEXT NOT NULL,
|
|
@@ -208,6 +228,32 @@ function setupBillingSchema(db) {
|
|
|
208
228
|
);
|
|
209
229
|
`);
|
|
210
230
|
}
|
|
231
|
+
function readMockPlanVersionFeatures(db, tenantId, planVersionId) {
|
|
232
|
+
try {
|
|
233
|
+
return db.prepare(`
|
|
234
|
+
SELECT feature_key, access, config_json
|
|
235
|
+
FROM plan_feature_matrix
|
|
236
|
+
WHERE tenant_id = ? AND plan_version_id = ?
|
|
237
|
+
`).all(tenantId, planVersionId).reduce((acc, row) => {
|
|
238
|
+
const featureKey = String(row?.feature_key || '').trim();
|
|
239
|
+
if (!featureKey)
|
|
240
|
+
return acc;
|
|
241
|
+
acc[featureKey] = {
|
|
242
|
+
featureKey,
|
|
243
|
+
access: String(row?.access || '').trim(),
|
|
244
|
+
config: row?.config_json ? JSON.parse(String(row.config_json)) : {}
|
|
245
|
+
};
|
|
246
|
+
return acc;
|
|
247
|
+
}, {});
|
|
248
|
+
}
|
|
249
|
+
catch (error) {
|
|
250
|
+
const message = String(error?.message || '');
|
|
251
|
+
if (message.toLowerCase().includes('no such table: plan_feature_matrix')) {
|
|
252
|
+
return {};
|
|
253
|
+
}
|
|
254
|
+
throw error;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
211
257
|
function buildBillingRouteHandlers(db, opts) {
|
|
212
258
|
const routes = new Map();
|
|
213
259
|
const tenantId = 'default';
|
|
@@ -288,12 +334,69 @@ function buildBillingRouteHandlers(db, opts) {
|
|
|
288
334
|
seatLimit: row?.seat_limit === null || row?.seat_limit === undefined ? null : Number(row.seat_limit),
|
|
289
335
|
trialDays: Number.isFinite(Number(metadata?.trialDays)) ? Number(metadata.trialDays) : 0,
|
|
290
336
|
metadata,
|
|
291
|
-
features:
|
|
337
|
+
features: readMockPlanVersionFeatures(db, tenantId, String(row?.plan_version_id || '')),
|
|
292
338
|
createdAt: '',
|
|
293
339
|
updatedAt: ''
|
|
294
340
|
};
|
|
295
341
|
});
|
|
296
342
|
},
|
|
343
|
+
listPlanFeatureCatalog: () => {
|
|
344
|
+
const overrides = db.prepare(`
|
|
345
|
+
SELECT feature_key, public_label, public_description, public_display_order
|
|
346
|
+
FROM plan_feature_catalog_overrides
|
|
347
|
+
WHERE tenant_id = ?
|
|
348
|
+
`).all(tenantId);
|
|
349
|
+
const overrideMap = new Map(overrides.map((row) => [
|
|
350
|
+
String(row.feature_key || '').trim(),
|
|
351
|
+
{
|
|
352
|
+
publicLabel: row.public_label ?? null,
|
|
353
|
+
publicDescription: row.public_description ?? null,
|
|
354
|
+
publicDisplayOrder: row.public_display_order ?? null
|
|
355
|
+
}
|
|
356
|
+
]));
|
|
357
|
+
return [
|
|
358
|
+
{
|
|
359
|
+
featureKey: 'collaboration.team_management',
|
|
360
|
+
label: 'Team Management',
|
|
361
|
+
description: 'Allow managing members, invites, roles, and collaboration settings.',
|
|
362
|
+
publicLabel: overrideMap.get('collaboration.team_management')?.publicLabel ?? null,
|
|
363
|
+
publicDescription: overrideMap.get('collaboration.team_management')?.publicDescription ?? null,
|
|
364
|
+
publicDisplayOrder: overrideMap.get('collaboration.team_management')?.publicDisplayOrder ?? 40,
|
|
365
|
+
allowedAccessModes: ['enabled', 'disabled'],
|
|
366
|
+
configTemplate: {}
|
|
367
|
+
},
|
|
368
|
+
{
|
|
369
|
+
featureKey: 'context.uploads',
|
|
370
|
+
label: 'Document Cloud Storage',
|
|
371
|
+
description: 'Allow cloud document storage with an optional storage allowance in MB.',
|
|
372
|
+
publicLabel: overrideMap.get('context.uploads')?.publicLabel ?? null,
|
|
373
|
+
publicDescription: overrideMap.get('context.uploads')?.publicDescription ?? null,
|
|
374
|
+
publicDisplayOrder: overrideMap.get('context.uploads')?.publicDisplayOrder ?? 30,
|
|
375
|
+
allowedAccessModes: ['enabled', 'disabled'],
|
|
376
|
+
configTemplate: { storageLimitMb: 1024 }
|
|
377
|
+
},
|
|
378
|
+
{
|
|
379
|
+
featureKey: 'workspace.workspaces',
|
|
380
|
+
label: 'Workspaces',
|
|
381
|
+
description: 'Allow multiple workspaces with a configurable workspace limit.',
|
|
382
|
+
publicLabel: overrideMap.get('workspace.workspaces')?.publicLabel ?? null,
|
|
383
|
+
publicDescription: overrideMap.get('workspace.workspaces')?.publicDescription ?? null,
|
|
384
|
+
publicDisplayOrder: overrideMap.get('workspace.workspaces')?.publicDisplayOrder ?? 10,
|
|
385
|
+
allowedAccessModes: ['enabled', 'disabled'],
|
|
386
|
+
configTemplate: { maxWorkspaces: 1 }
|
|
387
|
+
},
|
|
388
|
+
{
|
|
389
|
+
featureKey: 'collaboration.ai_profiles',
|
|
390
|
+
label: 'AI Profiles',
|
|
391
|
+
description: 'Allow registered AI collaborator profiles with an optional per-workspace limit.',
|
|
392
|
+
publicLabel: overrideMap.get('collaboration.ai_profiles')?.publicLabel ?? null,
|
|
393
|
+
publicDescription: overrideMap.get('collaboration.ai_profiles')?.publicDescription ?? null,
|
|
394
|
+
publicDisplayOrder: overrideMap.get('collaboration.ai_profiles')?.publicDisplayOrder ?? 20,
|
|
395
|
+
allowedAccessModes: ['enabled', 'limited', 'disabled'],
|
|
396
|
+
configTemplate: { maxAiProfiles: 1 }
|
|
397
|
+
}
|
|
398
|
+
];
|
|
399
|
+
},
|
|
297
400
|
getAccountEntitlement: () => {
|
|
298
401
|
const entitlement = opts?.accessStatus?.entitlement;
|
|
299
402
|
return {
|
|
@@ -691,6 +794,72 @@ describe('Billing Webhook Routes', () => {
|
|
|
691
794
|
`).get();
|
|
692
795
|
expect(String(failure?.reason || '')).toContain('no longer current for this account');
|
|
693
796
|
});
|
|
797
|
+
it('allows authoritative subscription webhooks to recover accounts after the local checkout expired', async () => {
|
|
798
|
+
const now = new Date().toISOString();
|
|
799
|
+
db.prepare(`
|
|
800
|
+
INSERT INTO plan_versions (plan_version_id, tenant_id, plan_id, version_number, is_default, metadata_json, created_at, updated_at)
|
|
801
|
+
VALUES ('team-v1', 'default', 'team', 1, 1, '{}', ?, ?)
|
|
802
|
+
`).run(now, now);
|
|
803
|
+
db.prepare(`
|
|
804
|
+
INSERT INTO billing_price_mappings (tenant_id, plan_version_id, billing_interval, stripe_price_id, active, created_at, updated_at)
|
|
805
|
+
VALUES ('default', 'team-v1', 'month', 'price_team_month', 1, ?, ?)
|
|
806
|
+
`).run(now, now);
|
|
807
|
+
db.prepare(`
|
|
808
|
+
INSERT INTO user_billing (
|
|
809
|
+
user_id, tenant_id, stripe_customer_id, entitlement_state, created_at, updated_at
|
|
810
|
+
) VALUES ('u1', 'default', 'cus_123', 'pending_plan_selection', ?, ?)
|
|
811
|
+
`).run(now, now);
|
|
812
|
+
db.prepare(`
|
|
813
|
+
INSERT INTO account_entitlements (
|
|
814
|
+
account_id, tenant_id, plan_version_id, entitlement_state, effective_at, effective_until, updated_by, created_at, updated_at
|
|
815
|
+
) VALUES ('u1', 'default', 'team-v1', 'pending_plan_selection', ?, NULL, 'checkout-expired', ?, ?)
|
|
816
|
+
`).run(now, now, now);
|
|
817
|
+
mockConstructEvent.mockReturnValue({
|
|
818
|
+
id: 'evt_checkout_expired_recovery',
|
|
819
|
+
type: 'customer.subscription.updated',
|
|
820
|
+
data: {
|
|
821
|
+
object: {
|
|
822
|
+
id: 'sub_123',
|
|
823
|
+
customer: 'cus_123',
|
|
824
|
+
status: 'active',
|
|
825
|
+
trial_end: null,
|
|
826
|
+
metadata: { planVersionId: 'team-v1' },
|
|
827
|
+
items: {
|
|
828
|
+
data: [{
|
|
829
|
+
current_period_end: Math.floor(Date.now() / 1000) + 86400,
|
|
830
|
+
price: { id: 'price_team_month', recurring: { interval: 'month' } }
|
|
831
|
+
}]
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
});
|
|
836
|
+
const req = createMockReq('{"id":"evt_checkout_expired_recovery"}', {
|
|
837
|
+
'x-taskforce-runtime-mode': 'cloud',
|
|
838
|
+
'stripe-signature': 'sig'
|
|
839
|
+
});
|
|
840
|
+
const res = createMockRes();
|
|
841
|
+
await webhookHandler(req, res, {});
|
|
842
|
+
expect(res.statusCode).toBe(200);
|
|
843
|
+
const entitlement = db.prepare(`
|
|
844
|
+
SELECT plan_version_id, entitlement_state, updated_by
|
|
845
|
+
FROM account_entitlements
|
|
846
|
+
WHERE account_id = 'u1' AND tenant_id = 'default'
|
|
847
|
+
LIMIT 1
|
|
848
|
+
`).get();
|
|
849
|
+
expect(entitlement).toEqual(expect.objectContaining({
|
|
850
|
+
plan_version_id: 'team-v1',
|
|
851
|
+
entitlement_state: 'active',
|
|
852
|
+
updated_by: 'stripe-webhook'
|
|
853
|
+
}));
|
|
854
|
+
const failure = db.prepare(`
|
|
855
|
+
SELECT reason
|
|
856
|
+
FROM stripe_webhook_failures
|
|
857
|
+
WHERE event_id = 'evt_checkout_expired_recovery'
|
|
858
|
+
ORDER BY created_at DESC
|
|
859
|
+
LIMIT 1
|
|
860
|
+
`).get();
|
|
861
|
+
expect(failure).toBeUndefined();
|
|
862
|
+
});
|
|
694
863
|
it('ignores stale subscription webhooks for active accounts when a newer checkout attempt is open', async () => {
|
|
695
864
|
const now = new Date().toISOString();
|
|
696
865
|
db.prepare(`
|
|
@@ -821,6 +990,9 @@ describe('Billing Webhook Routes', () => {
|
|
|
821
990
|
const firstCall = mockCheckoutCreate.mock.calls[0];
|
|
822
991
|
const args = (Array.isArray(firstCall) ? firstCall[0] : {});
|
|
823
992
|
expect(String(args.success_url || '')).toContain('/?screen=plans&checkout=success');
|
|
993
|
+
expect(String(args.success_url || '')).toContain('planId=collab');
|
|
994
|
+
expect(String(args.success_url || '')).toContain('planVersionId=collab-v1');
|
|
995
|
+
expect(String(args.success_url || '')).toContain('interval=month');
|
|
824
996
|
expect(String(args.cancel_url || '')).toContain('/?screen=plans&checkout=cancel');
|
|
825
997
|
expect(String(args.cancel_url || '')).toContain('planId=collab');
|
|
826
998
|
expect(String(args.cancel_url || '')).toContain('planVersionId=collab-v1');
|
|
@@ -1133,6 +1305,9 @@ describe('Billing Webhook Routes', () => {
|
|
|
1133
1305
|
expect(res.statusCode).toBe(200);
|
|
1134
1306
|
const args = mockCheckoutCreate.mock.calls.at(-1)?.[0];
|
|
1135
1307
|
expect(String(args.success_url || '')).toContain('http://localhost:5174/?screen=plans&checkout=success');
|
|
1308
|
+
expect(String(args.success_url || '')).toContain('planId=collab');
|
|
1309
|
+
expect(String(args.success_url || '')).toContain('planVersionId=collab-v1');
|
|
1310
|
+
expect(String(args.success_url || '')).toContain('interval=month');
|
|
1136
1311
|
expect(String(args.cancel_url || '')).toContain('http://localhost:5174/?screen=plans&checkout=cancel');
|
|
1137
1312
|
expect(JSON.parse(res.body)).toEqual(expect.objectContaining({
|
|
1138
1313
|
checkoutState: 'checkout',
|
|
@@ -1167,6 +1342,9 @@ describe('Billing Webhook Routes', () => {
|
|
|
1167
1342
|
expect(res.statusCode).toBe(200);
|
|
1168
1343
|
const args = mockCheckoutCreate.mock.calls.at(-1)?.[0];
|
|
1169
1344
|
expect(String(args.success_url || '')).toContain('http://localhost:5174/?screen=plans&checkout=success');
|
|
1345
|
+
expect(String(args.success_url || '')).toContain('planId=collab');
|
|
1346
|
+
expect(String(args.success_url || '')).toContain('planVersionId=collab-v1');
|
|
1347
|
+
expect(String(args.success_url || '')).toContain('interval=month');
|
|
1170
1348
|
});
|
|
1171
1349
|
it('builds direct cloud checkout return URLs from the actual request protocol when forwarded proto is absent', async () => {
|
|
1172
1350
|
const now = new Date().toISOString();
|
|
@@ -1192,6 +1370,9 @@ describe('Billing Webhook Routes', () => {
|
|
|
1192
1370
|
expect(res.statusCode).toBe(200);
|
|
1193
1371
|
const args = mockCheckoutCreate.mock.calls.at(-1)?.[0];
|
|
1194
1372
|
expect(String(args.success_url || '')).toContain('http://127.0.0.1:4173/?screen=plans&checkout=success');
|
|
1373
|
+
expect(String(args.success_url || '')).toContain('planId=collab');
|
|
1374
|
+
expect(String(args.success_url || '')).toContain('planVersionId=collab-v1');
|
|
1375
|
+
expect(String(args.success_url || '')).toContain('interval=month');
|
|
1195
1376
|
expect(String(args.cancel_url || '')).toContain('http://127.0.0.1:4173/?screen=plans&checkout=cancel');
|
|
1196
1377
|
});
|
|
1197
1378
|
it('applies a free plan immediately without creating a Stripe checkout session', async () => {
|
|
@@ -1822,6 +2003,118 @@ describe('Billing Webhook Routes', () => {
|
|
|
1822
2003
|
status: 'open'
|
|
1823
2004
|
}));
|
|
1824
2005
|
});
|
|
2006
|
+
it('does not mutate billing mirror or canonical entitlement when the purchased plan version is still unresolved', async () => {
|
|
2007
|
+
const now = new Date().toISOString();
|
|
2008
|
+
db.prepare(`
|
|
2009
|
+
INSERT INTO billing_checkout_attempts (
|
|
2010
|
+
session_id, tenant_id, user_id, plan_version_id, billing_interval, status, expires_at, created_at, updated_at
|
|
2011
|
+
) VALUES ('cs_plan_unresolved', 'default', 'u1', 'pro-v1', 'month', 'open', ?, ?, ?)
|
|
2012
|
+
`).run(new Date(Date.now() + 60 * 60 * 1000).toISOString(), now, now);
|
|
2013
|
+
db.prepare(`
|
|
2014
|
+
INSERT INTO account_entitlements (
|
|
2015
|
+
account_id, tenant_id, plan_version_id, entitlement_state, effective_at, updated_by, created_at, updated_at
|
|
2016
|
+
) VALUES ('u1', 'default', 'free-v1', 'active', ?, 'seed', ?, ?)
|
|
2017
|
+
`).run(now, now, now);
|
|
2018
|
+
db.prepare(`
|
|
2019
|
+
INSERT INTO user_billing (
|
|
2020
|
+
user_id, tenant_id, stripe_customer_id, stripe_subscription_id, tier, entitlement_state, created_at, updated_at
|
|
2021
|
+
) VALUES ('u1', 'default', 'cus_existing', 'sub_existing', 'free', 'active', ?, ?)
|
|
2022
|
+
`).run(now, now);
|
|
2023
|
+
mockCheckoutRetrieve.mockResolvedValueOnce({
|
|
2024
|
+
id: 'cs_plan_unresolved',
|
|
2025
|
+
mode: 'subscription',
|
|
2026
|
+
status: 'complete',
|
|
2027
|
+
payment_status: 'paid',
|
|
2028
|
+
customer: 'cus_123',
|
|
2029
|
+
subscription: 'sub_unmapped',
|
|
2030
|
+
metadata: { userId: 'u1' }
|
|
2031
|
+
});
|
|
2032
|
+
mockSubscriptionRetrieve.mockResolvedValueOnce({
|
|
2033
|
+
id: 'sub_unmapped',
|
|
2034
|
+
customer: 'cus_123',
|
|
2035
|
+
status: 'active',
|
|
2036
|
+
trial_end: null,
|
|
2037
|
+
metadata: { userId: 'u1' },
|
|
2038
|
+
items: {
|
|
2039
|
+
data: [{
|
|
2040
|
+
id: 'si_unmapped',
|
|
2041
|
+
current_period_end: Math.floor(Date.now() / 1000) + 86400,
|
|
2042
|
+
price: { id: 'price_unmapped', recurring: { interval: 'month' } }
|
|
2043
|
+
}]
|
|
2044
|
+
}
|
|
2045
|
+
});
|
|
2046
|
+
const confirmHandler = routeHandlers.get('POST /api/taskforce/billing/checkout-session/confirm');
|
|
2047
|
+
const req = createMockReq(JSON.stringify({
|
|
2048
|
+
sessionId: 'cs_plan_unresolved'
|
|
2049
|
+
}), {
|
|
2050
|
+
'x-taskforce-runtime-mode': 'cloud',
|
|
2051
|
+
host: 'app.taskforcehq.ai'
|
|
2052
|
+
});
|
|
2053
|
+
const res = createMockRes();
|
|
2054
|
+
await confirmHandler(req, res, {});
|
|
2055
|
+
expect(res.statusCode).toBe(409);
|
|
2056
|
+
expect(JSON.parse(res.body)).toEqual({
|
|
2057
|
+
error: 'Checkout completed, but the purchased plan could not be resolved yet.',
|
|
2058
|
+
code: 'CHECKOUT_PLAN_VERSION_UNRESOLVED'
|
|
2059
|
+
});
|
|
2060
|
+
const entitlement = db.prepare(`
|
|
2061
|
+
SELECT plan_version_id, entitlement_state
|
|
2062
|
+
FROM account_entitlements
|
|
2063
|
+
WHERE tenant_id = 'default' AND account_id = 'u1'
|
|
2064
|
+
LIMIT 1
|
|
2065
|
+
`).get();
|
|
2066
|
+
expect(entitlement).toEqual(expect.objectContaining({
|
|
2067
|
+
plan_version_id: 'free-v1',
|
|
2068
|
+
entitlement_state: 'active'
|
|
2069
|
+
}));
|
|
2070
|
+
const billingRow = db.prepare(`
|
|
2071
|
+
SELECT stripe_customer_id, stripe_subscription_id, tier, entitlement_state
|
|
2072
|
+
FROM user_billing
|
|
2073
|
+
WHERE tenant_id = 'default' AND user_id = 'u1'
|
|
2074
|
+
LIMIT 1
|
|
2075
|
+
`).get();
|
|
2076
|
+
expect(billingRow).toEqual(expect.objectContaining({
|
|
2077
|
+
stripe_customer_id: 'cus_existing',
|
|
2078
|
+
stripe_subscription_id: 'sub_existing',
|
|
2079
|
+
tier: 'free',
|
|
2080
|
+
entitlement_state: 'active'
|
|
2081
|
+
}));
|
|
2082
|
+
const attempt = db.prepare(`
|
|
2083
|
+
SELECT status
|
|
2084
|
+
FROM billing_checkout_attempts
|
|
2085
|
+
WHERE tenant_id = 'default' AND session_id = 'cs_plan_unresolved'
|
|
2086
|
+
LIMIT 1
|
|
2087
|
+
`).get();
|
|
2088
|
+
expect(attempt).toEqual(expect.objectContaining({
|
|
2089
|
+
status: 'open'
|
|
2090
|
+
}));
|
|
2091
|
+
});
|
|
2092
|
+
it('rejects checkout confirmation when the session belongs to a different account', async () => {
|
|
2093
|
+
mockCheckoutRetrieve.mockResolvedValueOnce({
|
|
2094
|
+
id: 'cs_wrong_account',
|
|
2095
|
+
mode: 'subscription',
|
|
2096
|
+
status: 'complete',
|
|
2097
|
+
payment_status: 'paid',
|
|
2098
|
+
customer: 'cus_other',
|
|
2099
|
+
subscription: 'sub_other',
|
|
2100
|
+
metadata: { userId: 'u2' }
|
|
2101
|
+
});
|
|
2102
|
+
const confirmHandler = routeHandlers.get('POST /api/taskforce/billing/checkout-session/confirm');
|
|
2103
|
+
const req = createMockReq(JSON.stringify({
|
|
2104
|
+
sessionId: 'cs_wrong_account'
|
|
2105
|
+
}), {
|
|
2106
|
+
'x-taskforce-runtime-mode': 'cloud',
|
|
2107
|
+
host: 'app.taskforcehq.ai'
|
|
2108
|
+
});
|
|
2109
|
+
const res = createMockRes();
|
|
2110
|
+
await confirmHandler(req, res, {});
|
|
2111
|
+
expect(res.statusCode).toBe(403);
|
|
2112
|
+
expect(JSON.parse(res.body)).toEqual({
|
|
2113
|
+
error: 'Checkout session does not belong to the current account.',
|
|
2114
|
+
code: 'CHECKOUT_SESSION_ACCOUNT_MISMATCH'
|
|
2115
|
+
});
|
|
2116
|
+
expect(mockSubscriptionRetrieve).not.toHaveBeenCalled();
|
|
2117
|
+
});
|
|
1825
2118
|
it('rejects checkout confirmation when a newer pending checkout superseded the session plan', async () => {
|
|
1826
2119
|
const now = new Date().toISOString();
|
|
1827
2120
|
db.prepare(`
|
|
@@ -2119,7 +2412,30 @@ describe('Billing Webhook Routes', () => {
|
|
|
2119
2412
|
`).get();
|
|
2120
2413
|
expect(purged).toBeUndefined();
|
|
2121
2414
|
});
|
|
2122
|
-
it('
|
|
2415
|
+
it('lists active Stripe price options for the requested interval', async () => {
|
|
2416
|
+
mockPricesList.mockResolvedValueOnce({
|
|
2417
|
+
data: [
|
|
2418
|
+
{
|
|
2419
|
+
id: 'price_month_1',
|
|
2420
|
+
active: true,
|
|
2421
|
+
currency: 'usd',
|
|
2422
|
+
unit_amount: 1900,
|
|
2423
|
+
nickname: 'Monthly Core',
|
|
2424
|
+
recurring: { interval: 'month' },
|
|
2425
|
+
product: { id: 'prod_core', name: 'Core' }
|
|
2426
|
+
},
|
|
2427
|
+
{
|
|
2428
|
+
id: 'price_year_1',
|
|
2429
|
+
active: true,
|
|
2430
|
+
currency: 'usd',
|
|
2431
|
+
unit_amount: 19000,
|
|
2432
|
+
nickname: 'Yearly Core',
|
|
2433
|
+
recurring: { interval: 'year' },
|
|
2434
|
+
product: { id: 'prod_core', name: 'Core' }
|
|
2435
|
+
}
|
|
2436
|
+
],
|
|
2437
|
+
has_more: false
|
|
2438
|
+
});
|
|
2123
2439
|
const route = routeHandlers.get('GET /api/taskforce/admin/billing/stripe-price-options');
|
|
2124
2440
|
expect(route).toBeTypeOf('function');
|
|
2125
2441
|
const req = createMockReq('', {
|
|
@@ -2128,9 +2444,19 @@ describe('Billing Webhook Routes', () => {
|
|
|
2128
2444
|
req.url = '/api/taskforce/admin/billing/stripe-price-options?interval=month';
|
|
2129
2445
|
const res = createMockRes();
|
|
2130
2446
|
await route(req, res, {});
|
|
2131
|
-
expect(res.statusCode).toBe(
|
|
2447
|
+
expect(res.statusCode).toBe(200);
|
|
2132
2448
|
const body = JSON.parse(res.body);
|
|
2133
|
-
expect(body.
|
|
2449
|
+
expect(body.options).toEqual([
|
|
2450
|
+
expect.objectContaining({
|
|
2451
|
+
stripePriceId: 'price_month_1',
|
|
2452
|
+
interval: 'month',
|
|
2453
|
+
unitAmount: 1900,
|
|
2454
|
+
currency: 'usd',
|
|
2455
|
+
nickname: 'Monthly Core',
|
|
2456
|
+
productId: 'prod_core',
|
|
2457
|
+
productName: 'Core'
|
|
2458
|
+
})
|
|
2459
|
+
]);
|
|
2134
2460
|
});
|
|
2135
2461
|
it('keeps active imported price mappings during Stripe import when the Stripe price is still active but lacks plan metadata', async () => {
|
|
2136
2462
|
const now = new Date().toISOString();
|
|
@@ -2171,12 +2497,23 @@ describe('Billing Webhook Routes', () => {
|
|
|
2171
2497
|
`).get();
|
|
2172
2498
|
expect(row.active).toBe(1);
|
|
2173
2499
|
});
|
|
2174
|
-
it('
|
|
2500
|
+
it('upserts manual Stripe price mappings and deactivates the prior active mapping', async () => {
|
|
2175
2501
|
const now = new Date().toISOString();
|
|
2176
2502
|
db.prepare(`
|
|
2177
2503
|
INSERT INTO plan_versions (plan_version_id, tenant_id, plan_id, version_number, is_default, metadata_json, created_at, updated_at)
|
|
2178
2504
|
VALUES ('collab-v1', 'default', 'collab', 1, 1, '{}', ?, ?)
|
|
2179
2505
|
`).run(now, now);
|
|
2506
|
+
db.prepare(`
|
|
2507
|
+
INSERT INTO billing_price_mappings (tenant_id, plan_version_id, billing_interval, stripe_price_id, pricing_type, active, unit_amount, currency, created_at, updated_at)
|
|
2508
|
+
VALUES ('default', 'collab-v1', 'month', 'price_old', 'stripe', 1, 1500, 'usd', ?, ?)
|
|
2509
|
+
`).run(now, now);
|
|
2510
|
+
mockPriceRetrieve.mockResolvedValueOnce({
|
|
2511
|
+
id: 'price_new',
|
|
2512
|
+
active: true,
|
|
2513
|
+
currency: 'usd',
|
|
2514
|
+
unit_amount: 2400,
|
|
2515
|
+
recurring: { interval: 'month' }
|
|
2516
|
+
});
|
|
2180
2517
|
const upsertRoute = routeHandlers.get('PUT /api/taskforce/admin/billing/price-mappings');
|
|
2181
2518
|
expect(upsertRoute).toBeTypeOf('function');
|
|
2182
2519
|
const upsertReq = createMockReq(JSON.stringify({
|
|
@@ -2187,10 +2524,27 @@ describe('Billing Webhook Routes', () => {
|
|
|
2187
2524
|
}), { 'x-taskforce-runtime-mode': 'cloud' });
|
|
2188
2525
|
const upsertRes = createMockRes();
|
|
2189
2526
|
await upsertRoute(upsertReq, upsertRes, {});
|
|
2190
|
-
expect(upsertRes.statusCode).toBe(
|
|
2191
|
-
|
|
2192
|
-
|
|
2527
|
+
expect(upsertRes.statusCode).toBe(200);
|
|
2528
|
+
const body = JSON.parse(upsertRes.body);
|
|
2529
|
+
expect(body.mapping).toEqual(expect.objectContaining({
|
|
2530
|
+
plan_version_id: 'collab-v1',
|
|
2531
|
+
billing_interval: 'month',
|
|
2532
|
+
stripe_price_id: 'price_new',
|
|
2533
|
+
pricing_type: 'stripe',
|
|
2534
|
+
active: 1,
|
|
2535
|
+
unit_amount: 2400,
|
|
2536
|
+
currency: 'usd'
|
|
2193
2537
|
}));
|
|
2538
|
+
const rows = db.prepare(`
|
|
2539
|
+
SELECT stripe_price_id, active, unit_amount
|
|
2540
|
+
FROM billing_price_mappings
|
|
2541
|
+
WHERE tenant_id = 'default' AND plan_version_id = 'collab-v1' AND billing_interval = 'month'
|
|
2542
|
+
ORDER BY stripe_price_id ASC
|
|
2543
|
+
`).all();
|
|
2544
|
+
expect(rows).toEqual([
|
|
2545
|
+
{ stripe_price_id: 'price_new', active: 1, unit_amount: 2400 },
|
|
2546
|
+
{ stripe_price_id: 'price_old', active: 0, unit_amount: 1500 }
|
|
2547
|
+
]);
|
|
2194
2548
|
});
|
|
2195
2549
|
it('rejects manual reuse of a Stripe price id across multiple plan versions', async () => {
|
|
2196
2550
|
const now = new Date().toISOString();
|
|
@@ -2211,7 +2565,19 @@ describe('Billing Webhook Routes', () => {
|
|
|
2211
2565
|
}), { 'x-taskforce-runtime-mode': 'cloud' });
|
|
2212
2566
|
const firstRes = createMockRes();
|
|
2213
2567
|
await upsertRoute(firstReq, firstRes, {});
|
|
2214
|
-
expect(firstRes.statusCode).toBe(
|
|
2568
|
+
expect(firstRes.statusCode).toBe(200);
|
|
2569
|
+
const secondReq = createMockReq(JSON.stringify({
|
|
2570
|
+
planVersionId: 'team-v1',
|
|
2571
|
+
interval: 'month',
|
|
2572
|
+
stripePriceId: 'price_shared',
|
|
2573
|
+
active: true
|
|
2574
|
+
}), { 'x-taskforce-runtime-mode': 'cloud' });
|
|
2575
|
+
const secondRes = createMockRes();
|
|
2576
|
+
await upsertRoute(secondReq, secondRes, {});
|
|
2577
|
+
expect(secondRes.statusCode).toBe(409);
|
|
2578
|
+
expect(JSON.parse(secondRes.body)).toEqual(expect.objectContaining({
|
|
2579
|
+
code: 'BILLING_PRICE_ALREADY_ASSIGNED'
|
|
2580
|
+
}));
|
|
2215
2581
|
});
|
|
2216
2582
|
it('keeps reused active mappings during Stripe import when the shared Stripe price is still active', async () => {
|
|
2217
2583
|
const now = new Date().toISOString();
|
|
@@ -2260,12 +2626,16 @@ describe('Billing Webhook Routes', () => {
|
|
|
2260
2626
|
{ plan_version_id: 'team-v1', stripe_price_id: 'price_shared', active: 1 }
|
|
2261
2627
|
]);
|
|
2262
2628
|
});
|
|
2263
|
-
it('
|
|
2629
|
+
it('supports manual free price mappings and deactivation', async () => {
|
|
2264
2630
|
const now = new Date().toISOString();
|
|
2265
2631
|
db.prepare(`
|
|
2266
2632
|
INSERT INTO plan_versions (plan_version_id, tenant_id, plan_id, version_number, is_default, metadata_json, created_at, updated_at)
|
|
2267
2633
|
VALUES ('starter-v1', 'default', 'starter', 1, 1, '{}', ?, ?)
|
|
2268
2634
|
`).run(now, now);
|
|
2635
|
+
db.prepare(`
|
|
2636
|
+
INSERT INTO billing_price_mappings (tenant_id, plan_version_id, billing_interval, stripe_price_id, pricing_type, active, unit_amount, currency, created_at, updated_at)
|
|
2637
|
+
VALUES ('default', 'starter-v1', 'month', 'price_old', 'stripe', 1, 1500, 'usd', ?, ?)
|
|
2638
|
+
`).run(now, now);
|
|
2269
2639
|
const upsertRoute = routeHandlers.get('PUT /api/taskforce/admin/billing/price-mappings');
|
|
2270
2640
|
expect(upsertRoute).toBeTypeOf('function');
|
|
2271
2641
|
const upsertReq = createMockReq(JSON.stringify({
|
|
@@ -2276,10 +2646,41 @@ describe('Billing Webhook Routes', () => {
|
|
|
2276
2646
|
}), { 'x-taskforce-runtime-mode': 'cloud' });
|
|
2277
2647
|
const upsertRes = createMockRes();
|
|
2278
2648
|
await upsertRoute(upsertReq, upsertRes, {});
|
|
2279
|
-
expect(upsertRes.statusCode).toBe(
|
|
2649
|
+
expect(upsertRes.statusCode).toBe(200);
|
|
2280
2650
|
expect(JSON.parse(upsertRes.body)).toEqual(expect.objectContaining({
|
|
2281
|
-
|
|
2651
|
+
mapping: expect.objectContaining({
|
|
2652
|
+
plan_version_id: 'starter-v1',
|
|
2653
|
+
billing_interval: 'month',
|
|
2654
|
+
stripe_price_id: 'free:starter-v1:month',
|
|
2655
|
+
pricing_type: 'free',
|
|
2656
|
+
active: 1,
|
|
2657
|
+
unit_amount: 0,
|
|
2658
|
+
currency: null
|
|
2659
|
+
})
|
|
2282
2660
|
}));
|
|
2661
|
+
const deactivateRoute = routeHandlers.get('POST /api/taskforce/admin/billing/price-mappings/deactivate');
|
|
2662
|
+
expect(deactivateRoute).toBeTypeOf('function');
|
|
2663
|
+
const deactivateReq = createMockReq(JSON.stringify({
|
|
2664
|
+
planVersionId: 'starter-v1',
|
|
2665
|
+
interval: 'month'
|
|
2666
|
+
}), { 'x-taskforce-runtime-mode': 'cloud' });
|
|
2667
|
+
const deactivateRes = createMockRes();
|
|
2668
|
+
await deactivateRoute(deactivateReq, deactivateRes, {});
|
|
2669
|
+
expect(deactivateRes.statusCode).toBe(200);
|
|
2670
|
+
expect(JSON.parse(deactivateRes.body)).toEqual(expect.objectContaining({
|
|
2671
|
+
success: true,
|
|
2672
|
+
changedRows: 2
|
|
2673
|
+
}));
|
|
2674
|
+
const rows = db.prepare(`
|
|
2675
|
+
SELECT stripe_price_id, pricing_type, active
|
|
2676
|
+
FROM billing_price_mappings
|
|
2677
|
+
WHERE tenant_id = 'default' AND plan_version_id = 'starter-v1' AND billing_interval = 'month'
|
|
2678
|
+
ORDER BY stripe_price_id ASC
|
|
2679
|
+
`).all();
|
|
2680
|
+
expect(rows).toEqual([
|
|
2681
|
+
{ stripe_price_id: 'free:starter-v1:month', pricing_type: 'free', active: 0 },
|
|
2682
|
+
{ stripe_price_id: 'price_old', pricing_type: 'stripe', active: 0 }
|
|
2683
|
+
]);
|
|
2283
2684
|
});
|
|
2284
2685
|
it('rejects webhook failure diagnostics for non-admin', async () => {
|
|
2285
2686
|
const nonAdminHandlers = buildBillingRouteHandlers(db, { isAdmin: false });
|
|
@@ -2357,6 +2758,185 @@ describe('Billing Webhook Routes', () => {
|
|
|
2357
2758
|
expect(freePlan.pricing.year).toBeNull();
|
|
2358
2759
|
expect(proPlan.defaultVersion.trialDays).toBe(0);
|
|
2359
2760
|
});
|
|
2761
|
+
it('invalidates cached public pricing when billing mappings change', async () => {
|
|
2762
|
+
const now = new Date().toISOString();
|
|
2763
|
+
routeHandlers = buildBillingRouteHandlers(db);
|
|
2764
|
+
const pricingRoute = routeHandlers.get('GET /api/taskforce/public/pricing');
|
|
2765
|
+
expect(pricingRoute).toBeTypeOf('function');
|
|
2766
|
+
db.prepare(`
|
|
2767
|
+
INSERT INTO plan_versions (plan_version_id, tenant_id, plan_id, version_number, is_default, metadata_json, created_at, updated_at)
|
|
2768
|
+
VALUES ('pro-v1', 'default', 'pro', 1, 1, '{}', ?, ?)
|
|
2769
|
+
`).run(now, now);
|
|
2770
|
+
db.prepare(`
|
|
2771
|
+
INSERT INTO billing_price_mappings (
|
|
2772
|
+
tenant_id, plan_version_id, billing_interval, stripe_price_id, pricing_type, active, unit_amount, currency, created_at, updated_at
|
|
2773
|
+
) VALUES ('default', 'pro-v1', 'month', 'price_pro_month', 'stripe', 1, 1200, 'usd', ?, ?)
|
|
2774
|
+
`).run(now, now);
|
|
2775
|
+
const firstReq = createMockReq('', { 'x-taskforce-runtime-mode': 'cloud' });
|
|
2776
|
+
const firstRes = createMockRes();
|
|
2777
|
+
await pricingRoute(firstReq, firstRes, {});
|
|
2778
|
+
expect(firstRes.statusCode).toBe(200);
|
|
2779
|
+
const firstBody = JSON.parse(firstRes.body);
|
|
2780
|
+
expect(firstBody.plans.find((p) => p.planId === 'pro')?.pricing?.month?.unitAmount).toBe(1200);
|
|
2781
|
+
const updatedAt = new Date(Date.now() + 1000).toISOString();
|
|
2782
|
+
db.prepare(`
|
|
2783
|
+
UPDATE billing_price_mappings
|
|
2784
|
+
SET unit_amount = 2400, updated_at = ?
|
|
2785
|
+
WHERE tenant_id = 'default' AND plan_version_id = 'pro-v1' AND billing_interval = 'month'
|
|
2786
|
+
`).run(updatedAt);
|
|
2787
|
+
const secondReq = createMockReq('', { 'x-taskforce-runtime-mode': 'cloud' });
|
|
2788
|
+
const secondRes = createMockRes();
|
|
2789
|
+
await pricingRoute(secondReq, secondRes, {});
|
|
2790
|
+
expect(secondRes.statusCode).toBe(200);
|
|
2791
|
+
const secondBody = JSON.parse(secondRes.body);
|
|
2792
|
+
expect(secondBody.plans.find((p) => p.planId === 'pro')?.pricing?.month?.unitAmount).toBe(2400);
|
|
2793
|
+
});
|
|
2794
|
+
it('invalidates cached public pricing when plan features change', async () => {
|
|
2795
|
+
const now = new Date().toISOString();
|
|
2796
|
+
routeHandlers = buildBillingRouteHandlers(db);
|
|
2797
|
+
const pricingRoute = routeHandlers.get('GET /api/taskforce/public/pricing');
|
|
2798
|
+
expect(pricingRoute).toBeTypeOf('function');
|
|
2799
|
+
db.exec(`
|
|
2800
|
+
CREATE TABLE IF NOT EXISTS plan_feature_matrix (
|
|
2801
|
+
tenant_id TEXT NOT NULL,
|
|
2802
|
+
plan_version_id TEXT NOT NULL,
|
|
2803
|
+
feature_key TEXT NOT NULL,
|
|
2804
|
+
access TEXT NOT NULL,
|
|
2805
|
+
config_json TEXT,
|
|
2806
|
+
created_at TEXT NOT NULL,
|
|
2807
|
+
updated_at TEXT NOT NULL,
|
|
2808
|
+
PRIMARY KEY (tenant_id, plan_version_id, feature_key)
|
|
2809
|
+
)
|
|
2810
|
+
`);
|
|
2811
|
+
db.prepare(`
|
|
2812
|
+
INSERT INTO plan_versions (plan_version_id, tenant_id, plan_id, version_number, is_default, metadata_json, created_at, updated_at)
|
|
2813
|
+
VALUES ('pro-v1', 'default', 'pro', 1, 1, '{}', ?, ?)
|
|
2814
|
+
`).run(now, now);
|
|
2815
|
+
db.prepare(`
|
|
2816
|
+
INSERT INTO billing_price_mappings (
|
|
2817
|
+
tenant_id, plan_version_id, billing_interval, stripe_price_id, pricing_type, active, unit_amount, currency, created_at, updated_at
|
|
2818
|
+
) VALUES ('default', 'pro-v1', 'month', 'price_pro_month', 'stripe', 1, 1200, 'usd', ?, ?)
|
|
2819
|
+
`).run(now, now);
|
|
2820
|
+
db.prepare(`
|
|
2821
|
+
INSERT INTO plan_feature_matrix (tenant_id, plan_version_id, feature_key, access, config_json, created_at, updated_at)
|
|
2822
|
+
VALUES ('default', 'pro-v1', 'ai.chat', 'enabled', '{}', ?, ?)
|
|
2823
|
+
`).run(now, now);
|
|
2824
|
+
const firstReq = createMockReq('', { 'x-taskforce-runtime-mode': 'cloud' });
|
|
2825
|
+
const firstRes = createMockRes();
|
|
2826
|
+
await pricingRoute(firstReq, firstRes, {});
|
|
2827
|
+
expect(firstRes.statusCode).toBe(200);
|
|
2828
|
+
const firstBody = JSON.parse(firstRes.body);
|
|
2829
|
+
expect(firstBody.plans.find((p) => p.planId === 'pro')?.features).toEqual([
|
|
2830
|
+
expect.objectContaining({ featureKey: 'ai.chat' })
|
|
2831
|
+
]);
|
|
2832
|
+
const updatedAt = new Date(Date.now() + 1000).toISOString();
|
|
2833
|
+
db.prepare(`
|
|
2834
|
+
UPDATE plan_feature_matrix
|
|
2835
|
+
SET access = 'disabled', updated_at = ?
|
|
2836
|
+
WHERE tenant_id = 'default' AND plan_version_id = 'pro-v1' AND feature_key = 'ai.chat'
|
|
2837
|
+
`).run(updatedAt);
|
|
2838
|
+
const secondReq = createMockReq('', { 'x-taskforce-runtime-mode': 'cloud' });
|
|
2839
|
+
const secondRes = createMockRes();
|
|
2840
|
+
await pricingRoute(secondReq, secondRes, {});
|
|
2841
|
+
expect(secondRes.statusCode).toBe(200);
|
|
2842
|
+
const secondBody = JSON.parse(secondRes.body);
|
|
2843
|
+
expect(secondBody.plans.find((p) => p.planId === 'pro')?.features).toEqual([]);
|
|
2844
|
+
});
|
|
2845
|
+
it('includes public feature copy and invalidates cached public pricing when catalog overrides change', async () => {
|
|
2846
|
+
const now = new Date().toISOString();
|
|
2847
|
+
routeHandlers = buildBillingRouteHandlers(db);
|
|
2848
|
+
const pricingRoute = routeHandlers.get('GET /api/taskforce/public/pricing');
|
|
2849
|
+
expect(pricingRoute).toBeTypeOf('function');
|
|
2850
|
+
db.prepare(`
|
|
2851
|
+
INSERT INTO plan_versions (plan_version_id, tenant_id, plan_id, version_number, is_default, metadata_json, created_at, updated_at)
|
|
2852
|
+
VALUES ('pro-v1', 'default', 'pro', 1, 1, '{}', ?, ?)
|
|
2853
|
+
`).run(now, now);
|
|
2854
|
+
db.prepare(`
|
|
2855
|
+
INSERT INTO billing_price_mappings (
|
|
2856
|
+
tenant_id, plan_version_id, billing_interval, stripe_price_id, pricing_type, active, unit_amount, currency, created_at, updated_at
|
|
2857
|
+
) VALUES ('default', 'pro-v1', 'month', 'price_pro_month', 'stripe', 1, 1200, 'usd', ?, ?)
|
|
2858
|
+
`).run(now, now);
|
|
2859
|
+
db.prepare(`
|
|
2860
|
+
INSERT INTO plan_feature_matrix (tenant_id, plan_version_id, feature_key, access, config_json, created_at, updated_at)
|
|
2861
|
+
VALUES ('default', 'pro-v1', 'collaboration.ai_profiles', 'enabled', '{}', ?, ?)
|
|
2862
|
+
`).run(now, now);
|
|
2863
|
+
db.prepare(`
|
|
2864
|
+
INSERT INTO plan_feature_catalog_overrides (tenant_id, feature_key, public_label, public_description, created_at, updated_at)
|
|
2865
|
+
VALUES ('default', 'collaboration.ai_profiles', 'AI teammates', 'Add reusable AI teammates to your workspace.', ?, ?)
|
|
2866
|
+
`).run(now, now);
|
|
2867
|
+
const firstReq = createMockReq('', { 'x-taskforce-runtime-mode': 'cloud' });
|
|
2868
|
+
const firstRes = createMockRes();
|
|
2869
|
+
await pricingRoute(firstReq, firstRes, {});
|
|
2870
|
+
expect(firstRes.statusCode).toBe(200);
|
|
2871
|
+
const firstBody = JSON.parse(firstRes.body);
|
|
2872
|
+
expect(firstBody.plans.find((p) => p.planId === 'pro')?.features).toEqual([
|
|
2873
|
+
expect.objectContaining({
|
|
2874
|
+
featureKey: 'collaboration.ai_profiles',
|
|
2875
|
+
label: 'AI Profiles',
|
|
2876
|
+
publicLabel: 'AI teammates',
|
|
2877
|
+
publicDescription: 'Add reusable AI teammates to your workspace.'
|
|
2878
|
+
})
|
|
2879
|
+
]);
|
|
2880
|
+
const updatedAt = new Date(Date.now() + 1000).toISOString();
|
|
2881
|
+
db.prepare(`
|
|
2882
|
+
UPDATE plan_feature_catalog_overrides
|
|
2883
|
+
SET public_label = 'AI copilots', updated_at = ?
|
|
2884
|
+
WHERE tenant_id = 'default' AND feature_key = 'collaboration.ai_profiles'
|
|
2885
|
+
`).run(updatedAt);
|
|
2886
|
+
const secondReq = createMockReq('', { 'x-taskforce-runtime-mode': 'cloud' });
|
|
2887
|
+
const secondRes = createMockRes();
|
|
2888
|
+
await pricingRoute(secondReq, secondRes, {});
|
|
2889
|
+
expect(secondRes.statusCode).toBe(200);
|
|
2890
|
+
const secondBody = JSON.parse(secondRes.body);
|
|
2891
|
+
expect(secondBody.plans.find((p) => p.planId === 'pro')?.features).toEqual([
|
|
2892
|
+
expect.objectContaining({
|
|
2893
|
+
featureKey: 'collaboration.ai_profiles',
|
|
2894
|
+
publicLabel: 'AI copilots'
|
|
2895
|
+
})
|
|
2896
|
+
]);
|
|
2897
|
+
});
|
|
2898
|
+
it('orders public pricing features by catalog display order', async () => {
|
|
2899
|
+
const now = new Date().toISOString();
|
|
2900
|
+
routeHandlers = buildBillingRouteHandlers(db);
|
|
2901
|
+
const pricingRoute = routeHandlers.get('GET /api/taskforce/public/pricing');
|
|
2902
|
+
expect(pricingRoute).toBeTypeOf('function');
|
|
2903
|
+
db.prepare(`
|
|
2904
|
+
INSERT INTO plan_versions (plan_version_id, tenant_id, plan_id, version_number, is_default, seat_limit, metadata_json, created_at, updated_at)
|
|
2905
|
+
VALUES ('pro-v1', 'default', 'pro', 1, 1, 5, '{}', ?, ?)
|
|
2906
|
+
`).run(now, now);
|
|
2907
|
+
db.prepare(`
|
|
2908
|
+
INSERT INTO billing_price_mappings (
|
|
2909
|
+
tenant_id, plan_version_id, billing_interval, stripe_price_id, pricing_type, active, unit_amount, currency, created_at, updated_at
|
|
2910
|
+
) VALUES ('default', 'pro-v1', 'month', 'price_pro_month', 'stripe', 1, 1800, 'usd', ?, ?)
|
|
2911
|
+
`).run(now, now);
|
|
2912
|
+
for (const feature of [
|
|
2913
|
+
{ key: 'collaboration.team_management', access: 'enabled', config: '{}' },
|
|
2914
|
+
{ key: 'collaboration.invites', access: 'enabled', config: '{}' },
|
|
2915
|
+
{ key: 'collaboration.seat_limit', access: 'limited', config: '{"seat_limit":5}' },
|
|
2916
|
+
{ key: 'context.uploads', access: 'enabled', config: '{"storageLimitMb":24}' },
|
|
2917
|
+
{ key: 'workspace.workspaces', access: 'enabled', config: '{"maxWorkspaces":1}' },
|
|
2918
|
+
{ key: 'collaboration.ai_profiles', access: 'limited', config: '{"maxAiProfiles":4}' }
|
|
2919
|
+
]) {
|
|
2920
|
+
db.prepare(`
|
|
2921
|
+
INSERT INTO plan_feature_matrix (tenant_id, plan_version_id, feature_key, access, config_json, created_at, updated_at)
|
|
2922
|
+
VALUES ('default', 'pro-v1', ?, ?, ?, ?, ?)
|
|
2923
|
+
`).run(feature.key, feature.access, feature.config, now, now);
|
|
2924
|
+
}
|
|
2925
|
+
const req = createMockReq('', { 'x-taskforce-runtime-mode': 'cloud' });
|
|
2926
|
+
const res = createMockRes();
|
|
2927
|
+
await pricingRoute(req, res, {});
|
|
2928
|
+
expect(res.statusCode).toBe(200);
|
|
2929
|
+
const body = JSON.parse(res.body);
|
|
2930
|
+
const orderedPlan = body.plans.find((plan) => plan.planId === 'pro');
|
|
2931
|
+
expect(orderedPlan.features.map((feature) => feature.featureKey)).toEqual([
|
|
2932
|
+
'workspace.workspaces',
|
|
2933
|
+
'collaboration.ai_profiles',
|
|
2934
|
+
'context.uploads',
|
|
2935
|
+
'collaboration.team_management'
|
|
2936
|
+
]);
|
|
2937
|
+
expect(orderedPlan.features.map((feature) => feature.featureKey)).not.toContain('collaboration.invites');
|
|
2938
|
+
expect(orderedPlan.features.map((feature) => feature.featureKey)).not.toContain('collaboration.seat_limit');
|
|
2939
|
+
});
|
|
2360
2940
|
it('returns the pricing environment derived from the cloud host', async () => {
|
|
2361
2941
|
routeHandlers = buildBillingRouteHandlers(db);
|
|
2362
2942
|
const pricingRoute = routeHandlers.get('GET /api/taskforce/public/pricing');
|