@taskforcehq/taskforce 0.3.304 → 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/README.md +140 -247
- package/dist/Taskforce.module.css +295 -89
- package/dist/TaskforceCore.js +378 -111
- package/dist/TaskforceCore.test.js +1221 -107
- 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 +52 -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/DocumentIndex.d.ts +1 -1
- package/dist/components/features/DocumentIndex.js +1 -1
- package/dist/components/features/DocumentViewer.d.ts +6 -2
- package/dist/components/features/DocumentViewer.js +90 -10
- package/dist/components/features/DocumentWorkspace.d.ts +5 -22
- package/dist/components/features/DocumentWorkspace.js +12 -155
- package/dist/components/features/DocumentWorkspace.test.js +226 -6
- package/dist/components/features/TaskSettings.js +29 -3
- package/dist/components/features/TaskSettings.test.js +42 -1
- 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/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.d.ts +8 -8
- package/dist/components/task/TaskForm.js +362 -230
- package/dist/components/task/TaskForm.test.js +558 -381
- package/dist/components/task/TaskKanban.d.ts +2 -1
- package/dist/components/task/TaskKanban.js +96 -14
- 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 -17
- package/dist/components/views/PlanComparisonPage.test.js +289 -41
- package/dist/components/views/PlansPage.js +239 -106
- package/dist/components/views/PlansPage.test.js +593 -42
- package/dist/components/views/StandaloneLayout.js +310 -554
- package/dist/components/views/panels/FilterToolbar.test.js +6 -4
- 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 +27 -14
- package/dist/components/views/standalone/modals/AccountHubModal.js +87 -15
- package/dist/components/views/standalone/modals/AccountHubModal.test.js +42 -2
- 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/config/envSchema.js +1 -1
- package/dist/core/AiProfileService.d.ts +45 -3
- package/dist/core/AiProfileService.js +774 -63
- package/dist/core/AiProfileService.test.js +22 -2
- package/dist/core/AiProfiles.test.js +761 -2
- 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.d.ts +1 -0
- package/dist/core/AuthTokenService.test.js +78 -0
- package/dist/core/EntitlementsPolicy.test.js +77 -15
- package/dist/core/GlobalSettingsService.js +115 -6
- package/dist/core/PlanEntitlementService.d.ts +30 -3
- package/dist/core/PlanEntitlementService.js +454 -93
- package/dist/core/PlanFeatureCatalog.test.d.ts +1 -0
- package/dist/core/PlanFeatureCatalog.test.js +84 -0
- package/dist/core/PlanVersionPolicy.test.js +54 -28
- package/dist/core/PlanningEntities.test.js +52 -0
- package/dist/core/SignupMonetizationSettings.test.js +241 -44
- package/dist/core/SyncReconciliationService.js +2 -65
- package/dist/core/SystemAdmin.test.js +244 -88
- package/dist/core/TaskAttachmentsCanonical.test.js +51 -1
- package/dist/core/TaskTaxonomyValidation.test.js +53 -0
- package/dist/core/Taskforce.d.ts +228 -14
- package/dist/core/Taskforce.js +814 -201
- 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 +128 -27
- package/dist/core/shared.d.ts +3 -1
- package/dist/core/shared.js +16 -1
- package/dist/core/types.d.ts +68 -2
- package/dist/hooks/auth/useTaskforceAuthBootstrap.js +26 -1
- package/dist/hooks/sync/auth.js +3 -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 +16 -3
- package/dist/hooks/sync/transfers.js +186 -65
- 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 +16 -6
- package/dist/hooks/useSyncOrchestrator.js +424 -175
- package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +487 -10
- package/dist/hooks/useTaskData.js +8 -3
- package/dist/hooks/useTaskData.test.js +45 -0
- package/dist/hooks/useTaskMutations.d.ts +2 -1
- package/dist/hooks/useTaskMutations.js +12 -1
- package/dist/hooks/useTaskMutations.test.js +30 -1
- package/dist/hooks/useTaskforce.d.ts +65 -4
- package/dist/hooks/useTaskforce.js +564 -243
- 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 +2454 -208
- package/dist/hooks/useWorkspaceSyncController.d.ts +4 -1
- package/dist/hooks/useWorkspaceSyncController.js +17 -2
- package/dist/hooks/useWorkspaceSyncController.test.js +1 -0
- 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/adminWorkspaceRegistrar.d.ts +2 -0
- package/dist/mcp/adminWorkspaceRegistrar.js +154 -0
- 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/runtime.d.ts +2 -0
- package/dist/mcp/runtime.js +2433 -3534
- package/dist/mcp/runtime.test.js +440 -2
- 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/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 +4 -5
- package/dist/server/routes/admin.js +410 -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 +16 -6
- package/dist/server/routes/auth.js +1015 -118
- 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 +276 -34
- package/dist/server/routes/billing.test.js +760 -52
- package/dist/server/routes/documentReviews.d.ts +1 -1
- package/dist/server/routes/documentReviews.js +1 -1
- package/dist/server/routes/documents.d.ts +5 -2
- package/dist/server/routes/documents.js +242 -74
- 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 +29 -3
- package/dist/server/routes/shared.js +69 -4
- package/dist/server/routes/sync.d.ts +1 -1
- package/dist/server/routes/sync.integration.test.js +437 -39
- package/dist/server/routes/sync.js +62 -606
- 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.d.ts +1 -1
- package/dist/server/routes/tasks.js +44 -2
- package/dist/server/routes/workspaces.d.ts +1 -1
- package/dist/server/routes/workspaces.js +37 -6
- package/dist/server/routes.d.ts +2 -9
- package/dist/server/routes.js +136 -165
- package/dist/server/routes.test.js +2457 -162
- 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 +33 -77
- package/dist/sync/syncApplyHandlers.d.ts +7 -0
- package/dist/sync/syncApplyHandlers.js +28 -13
- package/dist/sync/syncApplyHandlers.test.js +72 -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 +6 -0
- package/dist/sync/syncService.js +60 -0
- package/dist/sync/syncService.test.js +71 -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 +48 -0
- package/dist/sync/workspaceSyncModel.js +195 -31
- package/dist/sync/workspaceSyncModel.test.js +325 -12
- package/dist/sync/workspaceSyncState.d.ts +19 -0
- package/dist/sync/workspaceSyncState.js +3 -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-BlS-cqnl.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-BjR6iBf9.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-DZX4jk7e.js +9 -0
- package/dist/ui/assets/{WorkflowsModule-DnFqdUME.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-I7ZwG1z_.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.d.ts +19 -0
- package/dist/utils/commercialLifecyclePresentation.js +199 -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 +10 -2
- package/scripts/check-cloud-mcp.mjs +83 -0
- package/scripts/export-sqlite-to-postgres.mjs +7 -9
- package/scripts/playwright-auth.sh +5 -1
- package/scripts/run-billing-performance-smoke.sh +24 -0
- 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-BLWVbuKP.js +0 -1
- package/dist/ui/assets/DocumentWorkspace-BH_6DF6x.js +0 -250
- package/dist/ui/assets/DocumentWorkspace-C_8T8oz-.css +0 -1
- package/dist/ui/assets/PlansPage-C2dd2n1X.css +0 -1
- package/dist/ui/assets/PlansPage-CSDQ4sLa.js +0 -1
- package/dist/ui/assets/RussoOne-Regular-C3BxZIj7.ttf +0 -0
- package/dist/ui/assets/TaskSettings-CEDy34Jo.js +0 -9
- package/dist/ui/assets/index-DXUdtH1B.js +0 -6
- package/dist/ui/assets/index-Ii0B0rTO.css +0 -1
- 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 () => {
|
|
@@ -1415,7 +1596,7 @@ describe('Billing Webhook Routes', () => {
|
|
|
1415
1596
|
stripe_subscription_id: 'sub_email_match'
|
|
1416
1597
|
}));
|
|
1417
1598
|
});
|
|
1418
|
-
it('updates the existing paid subscription
|
|
1599
|
+
it('updates the existing paid subscription and keeps canonical entitlement aligned with the new plan', async () => {
|
|
1419
1600
|
const now = new Date().toISOString();
|
|
1420
1601
|
db.prepare(`
|
|
1421
1602
|
INSERT INTO plan_versions (plan_version_id, tenant_id, plan_id, version_number, is_default, metadata_json, created_at, updated_at)
|
|
@@ -1476,12 +1657,26 @@ describe('Billing Webhook Routes', () => {
|
|
|
1476
1657
|
planVersionId: 'team-v1'
|
|
1477
1658
|
}));
|
|
1478
1659
|
const entitlement = db.prepare(`
|
|
1479
|
-
SELECT plan_version_id
|
|
1660
|
+
SELECT plan_version_id, entitlement_state, updated_by
|
|
1480
1661
|
FROM account_entitlements
|
|
1481
1662
|
WHERE tenant_id = 'default' AND account_id = 'u1'
|
|
1482
1663
|
LIMIT 1
|
|
1483
1664
|
`).get();
|
|
1484
|
-
expect(entitlement
|
|
1665
|
+
expect(entitlement).toEqual(expect.objectContaining({
|
|
1666
|
+
plan_version_id: 'team-v1',
|
|
1667
|
+
entitlement_state: 'active',
|
|
1668
|
+
updated_by: 'checkout-confirm'
|
|
1669
|
+
}));
|
|
1670
|
+
const billingRow = db.prepare(`
|
|
1671
|
+
SELECT stripe_price_id, billing_interval
|
|
1672
|
+
FROM user_billing
|
|
1673
|
+
WHERE tenant_id = 'default' AND user_id = 'u1'
|
|
1674
|
+
LIMIT 1
|
|
1675
|
+
`).get();
|
|
1676
|
+
expect(billingRow).toEqual(expect.objectContaining({
|
|
1677
|
+
stripe_price_id: 'price_team_month',
|
|
1678
|
+
billing_interval: 'month'
|
|
1679
|
+
}));
|
|
1485
1680
|
});
|
|
1486
1681
|
it('updates the existing subscription when only the interval changes on the same plan', async () => {
|
|
1487
1682
|
const now = new Date().toISOString();
|
|
@@ -1544,7 +1739,7 @@ describe('Billing Webhook Routes', () => {
|
|
|
1544
1739
|
planVersionId: 'pro-v1'
|
|
1545
1740
|
}));
|
|
1546
1741
|
});
|
|
1547
|
-
it('confirms a successful checkout session and
|
|
1742
|
+
it('confirms a successful checkout session, activates canonical entitlement, and closes the checkout attempt', async () => {
|
|
1548
1743
|
const now = new Date().toISOString();
|
|
1549
1744
|
db.prepare(`
|
|
1550
1745
|
INSERT INTO plan_versions (plan_version_id, tenant_id, plan_id, version_number, is_default, metadata_json, created_at, updated_at)
|
|
@@ -1554,6 +1749,11 @@ describe('Billing Webhook Routes', () => {
|
|
|
1554
1749
|
INSERT INTO billing_price_mappings (tenant_id, plan_version_id, billing_interval, stripe_price_id, active, created_at, updated_at)
|
|
1555
1750
|
VALUES ('default', 'pro-v1', 'month', 'price_pro_month', 1, ?, ?)
|
|
1556
1751
|
`).run(now, now);
|
|
1752
|
+
db.prepare(`
|
|
1753
|
+
INSERT INTO billing_checkout_attempts (
|
|
1754
|
+
session_id, tenant_id, user_id, plan_version_id, billing_interval, status, expires_at, created_at, updated_at
|
|
1755
|
+
) VALUES ('cs_success_123', 'default', 'u1', 'pro-v1', 'month', 'open', ?, ?, ?)
|
|
1756
|
+
`).run(new Date(Date.now() + 60 * 60 * 1000).toISOString(), now, now);
|
|
1557
1757
|
const confirmHandler = routeHandlers.get('POST /api/taskforce/billing/checkout-session/confirm');
|
|
1558
1758
|
expect(confirmHandler).toBeTypeOf('function');
|
|
1559
1759
|
const req = createMockReq(JSON.stringify({
|
|
@@ -1570,14 +1770,36 @@ describe('Billing Webhook Routes', () => {
|
|
|
1570
1770
|
}));
|
|
1571
1771
|
expect(mockSubscriptionRetrieve).toHaveBeenCalledWith('sub_123');
|
|
1572
1772
|
const entitlement = db.prepare(`
|
|
1573
|
-
SELECT plan_version_id, entitlement_state
|
|
1773
|
+
SELECT plan_version_id, entitlement_state, updated_by
|
|
1574
1774
|
FROM account_entitlements
|
|
1575
1775
|
WHERE tenant_id = 'default' AND account_id = 'u1'
|
|
1576
1776
|
LIMIT 1
|
|
1577
1777
|
`).get();
|
|
1578
1778
|
expect(entitlement).toEqual(expect.objectContaining({
|
|
1579
1779
|
plan_version_id: 'pro-v1',
|
|
1580
|
-
entitlement_state: 'active'
|
|
1780
|
+
entitlement_state: 'active',
|
|
1781
|
+
updated_by: 'checkout-confirm'
|
|
1782
|
+
}));
|
|
1783
|
+
const billingRow = db.prepare(`
|
|
1784
|
+
SELECT stripe_customer_id, stripe_subscription_id, stripe_price_id, stripe_status
|
|
1785
|
+
FROM user_billing
|
|
1786
|
+
WHERE tenant_id = 'default' AND user_id = 'u1'
|
|
1787
|
+
LIMIT 1
|
|
1788
|
+
`).get();
|
|
1789
|
+
expect(billingRow).toEqual(expect.objectContaining({
|
|
1790
|
+
stripe_customer_id: 'cus_123',
|
|
1791
|
+
stripe_subscription_id: 'sub_123',
|
|
1792
|
+
stripe_price_id: 'price_pro_month',
|
|
1793
|
+
stripe_status: 'active'
|
|
1794
|
+
}));
|
|
1795
|
+
const attempt = db.prepare(`
|
|
1796
|
+
SELECT status
|
|
1797
|
+
FROM billing_checkout_attempts
|
|
1798
|
+
WHERE tenant_id = 'default' AND session_id = 'cs_success_123'
|
|
1799
|
+
LIMIT 1
|
|
1800
|
+
`).get();
|
|
1801
|
+
expect(attempt).toEqual(expect.objectContaining({
|
|
1802
|
+
status: 'confirmed'
|
|
1581
1803
|
}));
|
|
1582
1804
|
});
|
|
1583
1805
|
it('does not activate entitlement when the checkout session is still incomplete', async () => {
|
|
@@ -1631,6 +1853,268 @@ describe('Billing Webhook Routes', () => {
|
|
|
1631
1853
|
updated_by: 'checkout-start'
|
|
1632
1854
|
}));
|
|
1633
1855
|
});
|
|
1856
|
+
it('keeps checkout confirmation retryable until Stripe attaches the subscription to the completed session', async () => {
|
|
1857
|
+
const now = new Date().toISOString();
|
|
1858
|
+
db.prepare(`
|
|
1859
|
+
INSERT INTO plan_versions (plan_version_id, tenant_id, plan_id, version_number, is_default, metadata_json, created_at, updated_at)
|
|
1860
|
+
VALUES ('free-v1', 'default', 'free', 1, 1, '{}', ?, ?)
|
|
1861
|
+
`).run(now, now);
|
|
1862
|
+
db.prepare(`
|
|
1863
|
+
INSERT INTO plan_versions (plan_version_id, tenant_id, plan_id, version_number, is_default, metadata_json, created_at, updated_at)
|
|
1864
|
+
VALUES ('pro-v1', 'default', 'pro', 1, 0, '{}', ?, ?)
|
|
1865
|
+
`).run(now, now);
|
|
1866
|
+
db.prepare(`
|
|
1867
|
+
INSERT INTO account_entitlements (
|
|
1868
|
+
account_id, tenant_id, plan_version_id, entitlement_state, effective_at, updated_by, created_at, updated_at
|
|
1869
|
+
) VALUES ('u1', 'default', 'free-v1', 'active', ?, 'registration', ?, ?)
|
|
1870
|
+
`).run(now, now, now);
|
|
1871
|
+
db.prepare(`
|
|
1872
|
+
INSERT INTO billing_checkout_attempts (
|
|
1873
|
+
session_id, tenant_id, user_id, plan_version_id, billing_interval, status, expires_at, created_at, updated_at
|
|
1874
|
+
) VALUES ('cs_pending_subscription', 'default', 'u1', 'pro-v1', 'month', 'open', ?, ?, ?)
|
|
1875
|
+
`).run(new Date(Date.now() + 60 * 60 * 1000).toISOString(), now, now);
|
|
1876
|
+
mockCheckoutRetrieve.mockResolvedValueOnce({
|
|
1877
|
+
id: 'cs_pending_subscription',
|
|
1878
|
+
mode: 'subscription',
|
|
1879
|
+
status: 'complete',
|
|
1880
|
+
payment_status: 'paid',
|
|
1881
|
+
customer: 'cus_123',
|
|
1882
|
+
subscription: null,
|
|
1883
|
+
metadata: { userId: 'u1', planVersionId: 'pro-v1' }
|
|
1884
|
+
});
|
|
1885
|
+
const confirmHandler = routeHandlers.get('POST /api/taskforce/billing/checkout-session/confirm');
|
|
1886
|
+
const req = createMockReq(JSON.stringify({
|
|
1887
|
+
sessionId: 'cs_pending_subscription'
|
|
1888
|
+
}), {
|
|
1889
|
+
'x-taskforce-runtime-mode': 'cloud',
|
|
1890
|
+
host: 'app.taskforcehq.ai'
|
|
1891
|
+
});
|
|
1892
|
+
const res = createMockRes();
|
|
1893
|
+
await confirmHandler(req, res, {});
|
|
1894
|
+
expect(res.statusCode).toBe(409);
|
|
1895
|
+
expect(JSON.parse(res.body)).toEqual({
|
|
1896
|
+
error: 'Checkout session is still finalizing your subscription.',
|
|
1897
|
+
code: 'CHECKOUT_SESSION_INCOMPLETE'
|
|
1898
|
+
});
|
|
1899
|
+
expect(mockSubscriptionRetrieve).not.toHaveBeenCalled();
|
|
1900
|
+
const entitlement = db.prepare(`
|
|
1901
|
+
SELECT plan_version_id, entitlement_state
|
|
1902
|
+
FROM account_entitlements
|
|
1903
|
+
WHERE tenant_id = 'default' AND account_id = 'u1'
|
|
1904
|
+
LIMIT 1
|
|
1905
|
+
`).get();
|
|
1906
|
+
expect(entitlement).toEqual(expect.objectContaining({
|
|
1907
|
+
plan_version_id: 'free-v1',
|
|
1908
|
+
entitlement_state: 'active'
|
|
1909
|
+
}));
|
|
1910
|
+
const attempt = db.prepare(`
|
|
1911
|
+
SELECT status
|
|
1912
|
+
FROM billing_checkout_attempts
|
|
1913
|
+
WHERE tenant_id = 'default' AND session_id = 'cs_pending_subscription'
|
|
1914
|
+
LIMIT 1
|
|
1915
|
+
`).get();
|
|
1916
|
+
expect(attempt).toEqual(expect.objectContaining({
|
|
1917
|
+
status: 'open'
|
|
1918
|
+
}));
|
|
1919
|
+
});
|
|
1920
|
+
it('keeps checkout confirmation retryable until the subscription settles into an active commercial state', async () => {
|
|
1921
|
+
const now = new Date().toISOString();
|
|
1922
|
+
db.prepare(`
|
|
1923
|
+
INSERT INTO plan_versions (plan_version_id, tenant_id, plan_id, version_number, is_default, metadata_json, created_at, updated_at)
|
|
1924
|
+
VALUES ('free-v1', 'default', 'free', 1, 1, '{}', ?, ?)
|
|
1925
|
+
`).run(now, now);
|
|
1926
|
+
db.prepare(`
|
|
1927
|
+
INSERT INTO plan_versions (plan_version_id, tenant_id, plan_id, version_number, is_default, metadata_json, created_at, updated_at)
|
|
1928
|
+
VALUES ('pro-v1', 'default', 'pro', 1, 0, '{}', ?, ?)
|
|
1929
|
+
`).run(now, now);
|
|
1930
|
+
db.prepare(`
|
|
1931
|
+
INSERT INTO billing_price_mappings (tenant_id, plan_version_id, billing_interval, stripe_price_id, active, created_at, updated_at)
|
|
1932
|
+
VALUES ('default', 'pro-v1', 'month', 'price_pro_month', 1, ?, ?)
|
|
1933
|
+
`).run(now, now);
|
|
1934
|
+
db.prepare(`
|
|
1935
|
+
INSERT INTO user_billing (
|
|
1936
|
+
user_id, tenant_id, tier, entitlement_state, created_at, updated_at
|
|
1937
|
+
) VALUES ('u1', 'default', 'free', 'active', ?, ?)
|
|
1938
|
+
`).run(now, now);
|
|
1939
|
+
db.prepare(`
|
|
1940
|
+
INSERT INTO account_entitlements (
|
|
1941
|
+
account_id, tenant_id, plan_version_id, entitlement_state, effective_at, updated_by, created_at, updated_at
|
|
1942
|
+
) VALUES ('u1', 'default', 'free-v1', 'active', ?, 'registration', ?, ?)
|
|
1943
|
+
`).run(now, now, now);
|
|
1944
|
+
db.prepare(`
|
|
1945
|
+
INSERT INTO billing_checkout_attempts (
|
|
1946
|
+
session_id, tenant_id, user_id, plan_version_id, billing_interval, status, expires_at, created_at, updated_at
|
|
1947
|
+
) VALUES ('cs_wait_for_active', 'default', 'u1', 'pro-v1', 'month', 'open', ?, ?, ?)
|
|
1948
|
+
`).run(new Date(Date.now() + 60 * 60 * 1000).toISOString(), now, now);
|
|
1949
|
+
mockCheckoutRetrieve.mockResolvedValueOnce({
|
|
1950
|
+
id: 'cs_wait_for_active',
|
|
1951
|
+
mode: 'subscription',
|
|
1952
|
+
status: 'complete',
|
|
1953
|
+
payment_status: 'paid',
|
|
1954
|
+
customer: 'cus_123',
|
|
1955
|
+
subscription: 'sub_123',
|
|
1956
|
+
metadata: { userId: 'u1', planVersionId: 'pro-v1' }
|
|
1957
|
+
});
|
|
1958
|
+
mockSubscriptionRetrieve.mockResolvedValueOnce({
|
|
1959
|
+
id: 'sub_123',
|
|
1960
|
+
customer: 'cus_123',
|
|
1961
|
+
status: 'incomplete',
|
|
1962
|
+
trial_end: null,
|
|
1963
|
+
metadata: { userId: 'u1', planVersionId: 'pro-v1' },
|
|
1964
|
+
items: {
|
|
1965
|
+
data: [{
|
|
1966
|
+
id: 'si_123',
|
|
1967
|
+
current_period_end: Math.floor(Date.now() / 1000) + 86400,
|
|
1968
|
+
price: { id: 'price_pro_month', recurring: { interval: 'month' } }
|
|
1969
|
+
}]
|
|
1970
|
+
}
|
|
1971
|
+
});
|
|
1972
|
+
const confirmHandler = routeHandlers.get('POST /api/taskforce/billing/checkout-session/confirm');
|
|
1973
|
+
const req = createMockReq(JSON.stringify({
|
|
1974
|
+
sessionId: 'cs_wait_for_active'
|
|
1975
|
+
}), {
|
|
1976
|
+
'x-taskforce-runtime-mode': 'cloud',
|
|
1977
|
+
host: 'app.taskforcehq.ai'
|
|
1978
|
+
});
|
|
1979
|
+
const res = createMockRes();
|
|
1980
|
+
await confirmHandler(req, res, {});
|
|
1981
|
+
expect(res.statusCode).toBe(409);
|
|
1982
|
+
expect(JSON.parse(res.body)).toEqual({
|
|
1983
|
+
error: 'Checkout session is still finalizing your plan.',
|
|
1984
|
+
code: 'CHECKOUT_SESSION_INCOMPLETE'
|
|
1985
|
+
});
|
|
1986
|
+
const entitlement = db.prepare(`
|
|
1987
|
+
SELECT plan_version_id, entitlement_state
|
|
1988
|
+
FROM account_entitlements
|
|
1989
|
+
WHERE tenant_id = 'default' AND account_id = 'u1'
|
|
1990
|
+
LIMIT 1
|
|
1991
|
+
`).get();
|
|
1992
|
+
expect(entitlement).toEqual(expect.objectContaining({
|
|
1993
|
+
plan_version_id: 'free-v1',
|
|
1994
|
+
entitlement_state: 'active'
|
|
1995
|
+
}));
|
|
1996
|
+
const attempt = db.prepare(`
|
|
1997
|
+
SELECT status
|
|
1998
|
+
FROM billing_checkout_attempts
|
|
1999
|
+
WHERE tenant_id = 'default' AND session_id = 'cs_wait_for_active'
|
|
2000
|
+
LIMIT 1
|
|
2001
|
+
`).get();
|
|
2002
|
+
expect(attempt).toEqual(expect.objectContaining({
|
|
2003
|
+
status: 'open'
|
|
2004
|
+
}));
|
|
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
|
+
});
|
|
1634
2118
|
it('rejects checkout confirmation when a newer pending checkout superseded the session plan', async () => {
|
|
1635
2119
|
const now = new Date().toISOString();
|
|
1636
2120
|
db.prepare(`
|
|
@@ -1928,11 +2412,27 @@ describe('Billing Webhook Routes', () => {
|
|
|
1928
2412
|
`).get();
|
|
1929
2413
|
expect(purged).toBeUndefined();
|
|
1930
2414
|
});
|
|
1931
|
-
it('lists
|
|
2415
|
+
it('lists active Stripe price options for the requested interval', async () => {
|
|
1932
2416
|
mockPricesList.mockResolvedValueOnce({
|
|
1933
2417
|
data: [
|
|
1934
|
-
{
|
|
1935
|
-
|
|
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
|
+
}
|
|
1936
2436
|
],
|
|
1937
2437
|
has_more: false
|
|
1938
2438
|
});
|
|
@@ -1946,14 +2446,19 @@ describe('Billing Webhook Routes', () => {
|
|
|
1946
2446
|
await route(req, res, {});
|
|
1947
2447
|
expect(res.statusCode).toBe(200);
|
|
1948
2448
|
const body = JSON.parse(res.body);
|
|
1949
|
-
expect(
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
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
|
+
]);
|
|
1955
2460
|
});
|
|
1956
|
-
it('keeps active
|
|
2461
|
+
it('keeps active imported price mappings during Stripe import when the Stripe price is still active but lacks plan metadata', async () => {
|
|
1957
2462
|
const now = new Date().toISOString();
|
|
1958
2463
|
db.prepare(`
|
|
1959
2464
|
INSERT INTO plan_versions (plan_version_id, tenant_id, plan_id, version_number, is_default, metadata_json, created_at, updated_at)
|
|
@@ -1992,7 +2497,7 @@ describe('Billing Webhook Routes', () => {
|
|
|
1992
2497
|
`).get();
|
|
1993
2498
|
expect(row.active).toBe(1);
|
|
1994
2499
|
});
|
|
1995
|
-
it('upserts price mappings and deactivates prior active mapping
|
|
2500
|
+
it('upserts manual Stripe price mappings and deactivates the prior active mapping', async () => {
|
|
1996
2501
|
const now = new Date().toISOString();
|
|
1997
2502
|
db.prepare(`
|
|
1998
2503
|
INSERT INTO plan_versions (plan_version_id, tenant_id, plan_id, version_number, is_default, metadata_json, created_at, updated_at)
|
|
@@ -2000,13 +2505,13 @@ describe('Billing Webhook Routes', () => {
|
|
|
2000
2505
|
`).run(now, now);
|
|
2001
2506
|
db.prepare(`
|
|
2002
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)
|
|
2003
|
-
VALUES ('default', 'collab-v1', 'month', 'price_old', 'stripe', 1,
|
|
2508
|
+
VALUES ('default', 'collab-v1', 'month', 'price_old', 'stripe', 1, 1500, 'usd', ?, ?)
|
|
2004
2509
|
`).run(now, now);
|
|
2005
2510
|
mockPriceRetrieve.mockResolvedValueOnce({
|
|
2006
2511
|
id: 'price_new',
|
|
2007
2512
|
active: true,
|
|
2008
2513
|
currency: 'usd',
|
|
2009
|
-
unit_amount:
|
|
2514
|
+
unit_amount: 2400,
|
|
2010
2515
|
recurring: { interval: 'month' }
|
|
2011
2516
|
});
|
|
2012
2517
|
const upsertRoute = routeHandlers.get('PUT /api/taskforce/admin/billing/price-mappings');
|
|
@@ -2020,18 +2525,28 @@ describe('Billing Webhook Routes', () => {
|
|
|
2020
2525
|
const upsertRes = createMockRes();
|
|
2021
2526
|
await upsertRoute(upsertReq, upsertRes, {});
|
|
2022
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'
|
|
2537
|
+
}));
|
|
2023
2538
|
const rows = db.prepare(`
|
|
2024
|
-
SELECT stripe_price_id, active
|
|
2539
|
+
SELECT stripe_price_id, active, unit_amount
|
|
2025
2540
|
FROM billing_price_mappings
|
|
2026
2541
|
WHERE tenant_id = 'default' AND plan_version_id = 'collab-v1' AND billing_interval = 'month'
|
|
2027
2542
|
ORDER BY stripe_price_id ASC
|
|
2028
2543
|
`).all();
|
|
2029
2544
|
expect(rows).toEqual([
|
|
2030
|
-
{ stripe_price_id: 'price_new', active: 1 },
|
|
2031
|
-
{ stripe_price_id: 'price_old', active: 0 }
|
|
2545
|
+
{ stripe_price_id: 'price_new', active: 1, unit_amount: 2400 },
|
|
2546
|
+
{ stripe_price_id: 'price_old', active: 0, unit_amount: 1500 }
|
|
2032
2547
|
]);
|
|
2033
2548
|
});
|
|
2034
|
-
it('
|
|
2549
|
+
it('rejects manual reuse of a Stripe price id across multiple plan versions', async () => {
|
|
2035
2550
|
const now = new Date().toISOString();
|
|
2036
2551
|
db.prepare(`
|
|
2037
2552
|
INSERT INTO plan_versions (plan_version_id, tenant_id, plan_id, version_number, is_default, metadata_json, created_at, updated_at)
|
|
@@ -2041,13 +2556,6 @@ describe('Billing Webhook Routes', () => {
|
|
|
2041
2556
|
INSERT INTO plan_versions (plan_version_id, tenant_id, plan_id, version_number, is_default, metadata_json, created_at, updated_at)
|
|
2042
2557
|
VALUES ('team-v1', 'default', 'team', 1, 1, '{}', ?, ?)
|
|
2043
2558
|
`).run(now, now);
|
|
2044
|
-
mockPriceRetrieve.mockResolvedValue({
|
|
2045
|
-
id: 'price_shared',
|
|
2046
|
-
active: true,
|
|
2047
|
-
currency: 'usd',
|
|
2048
|
-
unit_amount: 1900,
|
|
2049
|
-
recurring: { interval: 'month' }
|
|
2050
|
-
});
|
|
2051
2559
|
const upsertRoute = routeHandlers.get('PUT /api/taskforce/admin/billing/price-mappings');
|
|
2052
2560
|
const firstReq = createMockReq(JSON.stringify({
|
|
2053
2561
|
planVersionId: 'collab-v1',
|
|
@@ -2066,19 +2574,12 @@ describe('Billing Webhook Routes', () => {
|
|
|
2066
2574
|
}), { 'x-taskforce-runtime-mode': 'cloud' });
|
|
2067
2575
|
const secondRes = createMockRes();
|
|
2068
2576
|
await upsertRoute(secondReq, secondRes, {});
|
|
2069
|
-
expect(secondRes.statusCode).toBe(
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
WHERE tenant_id = 'default' AND stripe_price_id = 'price_shared'
|
|
2074
|
-
ORDER BY plan_version_id ASC
|
|
2075
|
-
`).all();
|
|
2076
|
-
expect(rows).toEqual([
|
|
2077
|
-
{ plan_version_id: 'collab-v1', stripe_price_id: 'price_shared', active: 1 },
|
|
2078
|
-
{ plan_version_id: 'team-v1', stripe_price_id: 'price_shared', active: 1 }
|
|
2079
|
-
]);
|
|
2577
|
+
expect(secondRes.statusCode).toBe(409);
|
|
2578
|
+
expect(JSON.parse(secondRes.body)).toEqual(expect.objectContaining({
|
|
2579
|
+
code: 'BILLING_PRICE_ALREADY_ASSIGNED'
|
|
2580
|
+
}));
|
|
2080
2581
|
});
|
|
2081
|
-
it('keeps reused mappings
|
|
2582
|
+
it('keeps reused active mappings during Stripe import when the shared Stripe price is still active', async () => {
|
|
2082
2583
|
const now = new Date().toISOString();
|
|
2083
2584
|
db.prepare(`
|
|
2084
2585
|
INSERT INTO plan_versions (plan_version_id, tenant_id, plan_id, version_number, is_default, metadata_json, created_at, updated_at)
|
|
@@ -2125,12 +2626,16 @@ describe('Billing Webhook Routes', () => {
|
|
|
2125
2626
|
{ plan_version_id: 'team-v1', stripe_price_id: 'price_shared', active: 1 }
|
|
2126
2627
|
]);
|
|
2127
2628
|
});
|
|
2128
|
-
it('
|
|
2629
|
+
it('supports manual free price mappings and deactivation', async () => {
|
|
2129
2630
|
const now = new Date().toISOString();
|
|
2130
2631
|
db.prepare(`
|
|
2131
2632
|
INSERT INTO plan_versions (plan_version_id, tenant_id, plan_id, version_number, is_default, metadata_json, created_at, updated_at)
|
|
2132
2633
|
VALUES ('starter-v1', 'default', 'starter', 1, 1, '{}', ?, ?)
|
|
2133
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);
|
|
2134
2639
|
const upsertRoute = routeHandlers.get('PUT /api/taskforce/admin/billing/price-mappings');
|
|
2135
2640
|
expect(upsertRoute).toBeTypeOf('function');
|
|
2136
2641
|
const upsertReq = createMockReq(JSON.stringify({
|
|
@@ -2142,16 +2647,40 @@ describe('Billing Webhook Routes', () => {
|
|
|
2142
2647
|
const upsertRes = createMockRes();
|
|
2143
2648
|
await upsertRoute(upsertReq, upsertRes, {});
|
|
2144
2649
|
expect(upsertRes.statusCode).toBe(200);
|
|
2145
|
-
|
|
2146
|
-
|
|
2650
|
+
expect(JSON.parse(upsertRes.body)).toEqual(expect.objectContaining({
|
|
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
|
+
})
|
|
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
|
|
2147
2676
|
FROM billing_price_mappings
|
|
2148
2677
|
WHERE tenant_id = 'default' AND plan_version_id = 'starter-v1' AND billing_interval = 'month'
|
|
2149
|
-
|
|
2150
|
-
`).
|
|
2151
|
-
expect(
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
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
|
+
]);
|
|
2155
2684
|
});
|
|
2156
2685
|
it('rejects webhook failure diagnostics for non-admin', async () => {
|
|
2157
2686
|
const nonAdminHandlers = buildBillingRouteHandlers(db, { isAdmin: false });
|
|
@@ -2229,6 +2758,185 @@ describe('Billing Webhook Routes', () => {
|
|
|
2229
2758
|
expect(freePlan.pricing.year).toBeNull();
|
|
2230
2759
|
expect(proPlan.defaultVersion.trialDays).toBe(0);
|
|
2231
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
|
+
});
|
|
2232
2940
|
it('returns the pricing environment derived from the cloud host', async () => {
|
|
2233
2941
|
routeHandlers = buildBillingRouteHandlers(db);
|
|
2234
2942
|
const pricingRoute = routeHandlers.get('GET /api/taskforce/public/pricing');
|