@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
|
@@ -13,7 +13,7 @@ export declare function buildAuthFlowBehavior(core: TaskforceCore): {
|
|
|
13
13
|
};
|
|
14
14
|
export declare function buildAuthBehavior(core: TaskforceCore): {
|
|
15
15
|
onboardingPolicyMode: import("../../core/types.js").OnboardingPolicyMode;
|
|
16
|
-
|
|
16
|
+
onboardingPlanId: string | null;
|
|
17
17
|
allowSelfRegistration: boolean;
|
|
18
18
|
requireVerifiedEmail: boolean;
|
|
19
19
|
defaultNewUserBetaAccess: boolean;
|
|
@@ -45,7 +45,7 @@ export function buildAuthBehavior(core) {
|
|
|
45
45
|
return {
|
|
46
46
|
...authBehavior,
|
|
47
47
|
onboardingPolicyMode: onboardingPolicy.mode,
|
|
48
|
-
|
|
48
|
+
onboardingPlanId: onboardingPolicy.planId
|
|
49
49
|
};
|
|
50
50
|
}
|
|
51
51
|
export function buildSiteBehavior(core) {
|
|
@@ -8,16 +8,63 @@ const BILLING_DIAGNOSTIC_RETENTION_DAYS = 30;
|
|
|
8
8
|
const BILLING_DIAGNOSTIC_MAX_PAYLOAD_BYTES = 8 * 1024;
|
|
9
9
|
const BILLING_CHECKOUT_ATTEMPT_RETENTION_DAYS = 14;
|
|
10
10
|
const BILLING_STATUS_RECONCILIATION_TTL_MS = 30_000;
|
|
11
|
+
const PUBLIC_PRICING_CACHE_TTL_MS = 60_000;
|
|
11
12
|
const TASKFORCE_PROXY_ORIGIN_HOST_HEADER = 'x-taskforce-origin-host';
|
|
12
13
|
const TASKFORCE_PROXY_ORIGIN_PROTO_HEADER = 'x-taskforce-origin-proto';
|
|
13
14
|
const TASKFORCE_PROXY_ORIGIN_BASE_URL_HEADER = 'x-taskforce-origin-base-url';
|
|
14
15
|
const TASKFORCE_PROXY_ORIGIN_RUNTIME_HEADER = 'x-taskforce-origin-runtime';
|
|
16
|
+
const INTERNAL_PUBLIC_PRICING_FEATURE_KEYS = new Set([
|
|
17
|
+
'collaboration.invites',
|
|
18
|
+
'collaboration.seat_limit'
|
|
19
|
+
]);
|
|
20
|
+
const publicPricingCache = new Map();
|
|
15
21
|
function parsePricingType(raw) {
|
|
16
22
|
const value = String(raw || '').trim().toLowerCase();
|
|
17
23
|
if (value === 'stripe' || value === 'free')
|
|
18
24
|
return value;
|
|
19
25
|
return null;
|
|
20
26
|
}
|
|
27
|
+
function readTableVersionStats(core, tableName) {
|
|
28
|
+
const db = core.getDatabaseAdapter();
|
|
29
|
+
const tenantId = core.getTenantId();
|
|
30
|
+
try {
|
|
31
|
+
const row = db.prepare(`
|
|
32
|
+
SELECT COUNT(*) AS count, MAX(updated_at) AS max_updated_at
|
|
33
|
+
FROM ${tableName}
|
|
34
|
+
WHERE tenant_id = ?
|
|
35
|
+
`).get(tenantId);
|
|
36
|
+
return {
|
|
37
|
+
count: Number(row?.count || 0),
|
|
38
|
+
maxUpdatedAt: String(row?.max_updated_at || '')
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
const message = String(error?.message || '');
|
|
43
|
+
if (message.toLowerCase().includes(`no such table: ${tableName}`)) {
|
|
44
|
+
return { count: 0, maxUpdatedAt: '' };
|
|
45
|
+
}
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function buildPublicPricingCacheVersion(core) {
|
|
50
|
+
const catalog = readTableVersionStats(core, 'plan_catalog');
|
|
51
|
+
const versions = readTableVersionStats(core, 'plan_versions');
|
|
52
|
+
const prices = readTableVersionStats(core, 'billing_price_mappings');
|
|
53
|
+
const features = readTableVersionStats(core, 'plan_feature_matrix');
|
|
54
|
+
const featureCatalogOverrides = readTableVersionStats(core, 'plan_feature_catalog_overrides');
|
|
55
|
+
return [
|
|
56
|
+
catalog.count,
|
|
57
|
+
catalog.maxUpdatedAt,
|
|
58
|
+
versions.count,
|
|
59
|
+
versions.maxUpdatedAt,
|
|
60
|
+
prices.count,
|
|
61
|
+
prices.maxUpdatedAt,
|
|
62
|
+
features.count,
|
|
63
|
+
features.maxUpdatedAt,
|
|
64
|
+
featureCatalogOverrides.count,
|
|
65
|
+
featureCatalogOverrides.maxUpdatedAt
|
|
66
|
+
].join('|');
|
|
67
|
+
}
|
|
21
68
|
function buildFreePricingSyntheticId(planVersionId, interval) {
|
|
22
69
|
return `free:${planVersionId}:${interval}`;
|
|
23
70
|
}
|
|
@@ -664,7 +711,11 @@ function resolveCheckoutLifecycleGuard(core, input) {
|
|
|
664
711
|
: currentOpenAttempt;
|
|
665
712
|
const attemptStatus = String(checkoutAttempt?.status || '').trim().toLowerCase();
|
|
666
713
|
const attemptPlanVersionId = String(checkoutAttempt?.plan_version_id || '').trim();
|
|
714
|
+
const sourceLabel = String(input.source || 'checkout-confirm').trim() || 'checkout-confirm';
|
|
667
715
|
if (attemptStatus === 'expired') {
|
|
716
|
+
console.warn(`[Taskforce Billing] Lifecycle guard blocked expired checkout for user=${input.userId} source=${sourceLabel} `
|
|
717
|
+
+ `session=${String(input.sessionId || '').trim() || '(none)'} subscription=${String(input.subscriptionId || '').trim() || '(none)'} `
|
|
718
|
+
+ `requestedPlanVersionId=${String(input.planVersionId || '').trim() || '(none)'} attemptPlanVersionId=${attemptPlanVersionId || '(none)'}`);
|
|
668
719
|
return {
|
|
669
720
|
blocked: true,
|
|
670
721
|
code: 'CHECKOUT_SESSION_EXPIRED',
|
|
@@ -672,6 +723,9 @@ function resolveCheckoutLifecycleGuard(core, input) {
|
|
|
672
723
|
};
|
|
673
724
|
}
|
|
674
725
|
if (attemptStatus === 'superseded') {
|
|
726
|
+
console.warn(`[Taskforce Billing] Lifecycle guard blocked superseded checkout for user=${input.userId} source=${sourceLabel} `
|
|
727
|
+
+ `session=${String(input.sessionId || '').trim() || '(none)'} subscription=${String(input.subscriptionId || '').trim() || '(none)'} `
|
|
728
|
+
+ `requestedPlanVersionId=${String(input.planVersionId || '').trim() || '(none)'} attemptPlanVersionId=${attemptPlanVersionId || '(none)'}`);
|
|
675
729
|
return {
|
|
676
730
|
blocked: true,
|
|
677
731
|
code: 'CHECKOUT_SESSION_SUPERSEDED',
|
|
@@ -679,6 +733,9 @@ function resolveCheckoutLifecycleGuard(core, input) {
|
|
|
679
733
|
};
|
|
680
734
|
}
|
|
681
735
|
if (attemptStatus === 'open' && attemptPlanVersionId && input.planVersionId && attemptPlanVersionId !== input.planVersionId) {
|
|
736
|
+
console.warn(`[Taskforce Billing] Lifecycle guard blocked mismatched open checkout for user=${input.userId} source=${sourceLabel} `
|
|
737
|
+
+ `session=${String(input.sessionId || '').trim() || '(none)'} subscription=${String(input.subscriptionId || '').trim() || '(none)'} `
|
|
738
|
+
+ `requestedPlanVersionId=${String(input.planVersionId || '').trim() || '(none)'} attemptPlanVersionId=${attemptPlanVersionId}`);
|
|
682
739
|
return {
|
|
683
740
|
blocked: true,
|
|
684
741
|
code: 'CHECKOUT_SESSION_SUPERSEDED',
|
|
@@ -688,6 +745,9 @@ function resolveCheckoutLifecycleGuard(core, input) {
|
|
|
688
745
|
const currentBilling = getBillingMirrorRow(core, input.userId);
|
|
689
746
|
const currentSubscriptionId = String(currentBilling?.stripe_subscription_id || '').trim();
|
|
690
747
|
if (!currentOpenAttempt && input.subscriptionId && currentSubscriptionId && currentSubscriptionId !== input.subscriptionId) {
|
|
748
|
+
console.warn(`[Taskforce Billing] Lifecycle guard blocked stale subscription linkage for user=${input.userId} source=${sourceLabel} `
|
|
749
|
+
+ `currentSubscriptionId=${currentSubscriptionId} requestedSubscriptionId=${String(input.subscriptionId || '').trim() || '(none)'} `
|
|
750
|
+
+ `requestedPlanVersionId=${String(input.planVersionId || '').trim() || '(none)'}`);
|
|
691
751
|
return {
|
|
692
752
|
blocked: true,
|
|
693
753
|
code: 'CHECKOUT_SESSION_SUPERSEDED',
|
|
@@ -701,17 +761,28 @@ function resolveCheckoutLifecycleGuard(core, input) {
|
|
|
701
761
|
const currentPlanVersionId = String(entitlement.plan_version_id || '').trim();
|
|
702
762
|
const updatedBy = String(entitlement.updated_by || '').trim().toLowerCase();
|
|
703
763
|
const expiredPendingCheckout = currentState === 'checkout_pending' && isIsoTimestampExpired(entitlement.effective_until);
|
|
704
|
-
|
|
764
|
+
const fromStripeWebhook = input.source === 'stripe-webhook';
|
|
765
|
+
if (!fromStripeWebhook && (expiredPendingCheckout || (currentState === 'pending_plan_selection' && updatedBy === 'checkout-expired'))) {
|
|
766
|
+
console.warn(`[Taskforce Billing] Lifecycle guard blocked expired entitlement recovery for user=${input.userId} source=${sourceLabel} `
|
|
767
|
+
+ `entitlementState=${currentState || '(none)'} updatedBy=${updatedBy || '(none)'} `
|
|
768
|
+
+ `requestedPlanVersionId=${String(input.planVersionId || '').trim() || '(none)'} currentPlanVersionId=${currentPlanVersionId || '(none)'}`);
|
|
705
769
|
return {
|
|
706
770
|
blocked: true,
|
|
707
771
|
code: 'CHECKOUT_SESSION_EXPIRED',
|
|
708
772
|
message: 'This checkout session expired. Choose a plan and start checkout again.'
|
|
709
773
|
};
|
|
710
774
|
}
|
|
775
|
+
if (fromStripeWebhook && (expiredPendingCheckout || (currentState === 'pending_plan_selection' && updatedBy === 'checkout-expired'))) {
|
|
776
|
+
console.info(`[Taskforce Billing] Lifecycle guard allowed authoritative webhook recovery for user=${input.userId} `
|
|
777
|
+
+ `subscription=${String(input.subscriptionId || '').trim() || '(none)'} requestedPlanVersionId=${String(input.planVersionId || '').trim() || '(none)'} `
|
|
778
|
+
+ `entitlementState=${currentState || '(none)'} updatedBy=${updatedBy || '(none)'}`);
|
|
779
|
+
}
|
|
711
780
|
if (currentState === 'checkout_pending'
|
|
712
781
|
&& currentPlanVersionId
|
|
713
782
|
&& input.planVersionId
|
|
714
783
|
&& currentPlanVersionId !== input.planVersionId) {
|
|
784
|
+
console.warn(`[Taskforce Billing] Lifecycle guard blocked superseded checkout_pending entitlement for user=${input.userId} source=${sourceLabel} `
|
|
785
|
+
+ `currentPlanVersionId=${currentPlanVersionId} requestedPlanVersionId=${String(input.planVersionId || '').trim() || '(none)'}`);
|
|
715
786
|
return {
|
|
716
787
|
blocked: true,
|
|
717
788
|
code: 'CHECKOUT_SESSION_SUPERSEDED',
|
|
@@ -1330,11 +1401,18 @@ export function registerBillingRoutes(deps) {
|
|
|
1330
1401
|
res.end(JSON.stringify({ error: 'Forbidden: system admin role required.' }));
|
|
1331
1402
|
return;
|
|
1332
1403
|
}
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1404
|
+
try {
|
|
1405
|
+
const requestUrl = parseRequestUrl(req);
|
|
1406
|
+
const interval = parseInterval(requestUrl.searchParams.get('interval'));
|
|
1407
|
+
const options = await listStripePriceOptions(interval);
|
|
1408
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1409
|
+
res.end(JSON.stringify({ options }));
|
|
1410
|
+
}
|
|
1411
|
+
catch (error) {
|
|
1412
|
+
const statusCode = resolveErrorStatusCode(error, 400);
|
|
1413
|
+
res.writeHead(statusCode, { 'Content-Type': 'application/json' });
|
|
1414
|
+
res.end(JSON.stringify({ error: String(error?.message || 'Unable to list Stripe prices'), code: error?.code }));
|
|
1415
|
+
}
|
|
1338
1416
|
});
|
|
1339
1417
|
addRoute('PUT', '/api/taskforce/admin/billing/price-mappings', async (req, res) => {
|
|
1340
1418
|
if (!ensureCloudBilling(req, res))
|
|
@@ -1344,11 +1422,138 @@ export function registerBillingRoutes(deps) {
|
|
|
1344
1422
|
res.end(JSON.stringify({ error: 'Forbidden: system admin role required.' }));
|
|
1345
1423
|
return;
|
|
1346
1424
|
}
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1425
|
+
const body = await parseJsonBody(req);
|
|
1426
|
+
const planVersionId = String(body?.planVersionId || '').trim();
|
|
1427
|
+
const interval = parseInterval(body?.interval);
|
|
1428
|
+
const pricingType = parsePricingType(body?.pricingType) || 'stripe';
|
|
1429
|
+
const stripePriceId = String(body?.stripePriceId || '').trim();
|
|
1430
|
+
const setActive = body?.active === false ? false : true;
|
|
1431
|
+
if (!planVersionId || !interval) {
|
|
1432
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
1433
|
+
res.end(JSON.stringify({ error: 'planVersionId and interval are required.', code: 'BILLING_MAPPING_INPUT_REQUIRED' }));
|
|
1434
|
+
return;
|
|
1435
|
+
}
|
|
1436
|
+
if (pricingType === 'stripe' && !stripePriceId) {
|
|
1437
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
1438
|
+
res.end(JSON.stringify({ error: 'stripePriceId is required for Stripe pricing.', code: 'BILLING_MAPPING_STRIPE_PRICE_REQUIRED' }));
|
|
1439
|
+
return;
|
|
1440
|
+
}
|
|
1441
|
+
const db = core.getDatabaseAdapter();
|
|
1442
|
+
const tenantId = core.getTenantId();
|
|
1443
|
+
const versionExists = db.prepare(`
|
|
1444
|
+
SELECT plan_version_id
|
|
1445
|
+
FROM plan_versions
|
|
1446
|
+
WHERE tenant_id = ? AND plan_version_id = ?
|
|
1447
|
+
LIMIT 1
|
|
1448
|
+
`).get(tenantId, planVersionId);
|
|
1449
|
+
if (!versionExists?.plan_version_id) {
|
|
1450
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
1451
|
+
res.end(JSON.stringify({ error: 'Plan version not found.', code: 'PLAN_VERSION_NOT_FOUND' }));
|
|
1452
|
+
return;
|
|
1453
|
+
}
|
|
1454
|
+
try {
|
|
1455
|
+
const now = new Date().toISOString();
|
|
1456
|
+
const targetStripePriceId = pricingType === 'free'
|
|
1457
|
+
? buildFreePricingSyntheticId(planVersionId, interval)
|
|
1458
|
+
: stripePriceId;
|
|
1459
|
+
let unitAmount = 0;
|
|
1460
|
+
let currency = null;
|
|
1461
|
+
if (pricingType === 'stripe') {
|
|
1462
|
+
const stripe = getStripeClient();
|
|
1463
|
+
const stripePrice = await stripe.prices.retrieve(stripePriceId);
|
|
1464
|
+
const stripeInterval = parseInterval(stripePrice.recurring?.interval || '');
|
|
1465
|
+
if (!stripePrice.active) {
|
|
1466
|
+
res.writeHead(422, { 'Content-Type': 'application/json' });
|
|
1467
|
+
res.end(JSON.stringify({ error: 'Stripe price is not active.', code: 'STRIPE_PRICE_INACTIVE' }));
|
|
1468
|
+
return;
|
|
1469
|
+
}
|
|
1470
|
+
if (!stripeInterval) {
|
|
1471
|
+
res.writeHead(422, { 'Content-Type': 'application/json' });
|
|
1472
|
+
res.end(JSON.stringify({ error: 'Stripe price must be recurring month/year.', code: 'STRIPE_PRICE_INTERVAL_UNSUPPORTED' }));
|
|
1473
|
+
return;
|
|
1474
|
+
}
|
|
1475
|
+
if (stripeInterval !== interval) {
|
|
1476
|
+
res.writeHead(422, { 'Content-Type': 'application/json' });
|
|
1477
|
+
res.end(JSON.stringify({ error: `Stripe price interval (${stripeInterval}) does not match requested interval (${interval}).`, code: 'STRIPE_PRICE_INTERVAL_MISMATCH' }));
|
|
1478
|
+
return;
|
|
1479
|
+
}
|
|
1480
|
+
const conflictingMapping = db.prepare(`
|
|
1481
|
+
SELECT plan_version_id
|
|
1482
|
+
FROM billing_price_mappings
|
|
1483
|
+
WHERE tenant_id = ?
|
|
1484
|
+
AND stripe_price_id = ?
|
|
1485
|
+
AND active = 1
|
|
1486
|
+
AND plan_version_id <> ?
|
|
1487
|
+
LIMIT 1
|
|
1488
|
+
`).get(tenantId, stripePriceId, planVersionId);
|
|
1489
|
+
if (conflictingMapping?.plan_version_id) {
|
|
1490
|
+
res.writeHead(409, { 'Content-Type': 'application/json' });
|
|
1491
|
+
res.end(JSON.stringify({
|
|
1492
|
+
error: `Stripe price ${stripePriceId} is already mapped to ${String(conflictingMapping.plan_version_id)}.`,
|
|
1493
|
+
code: 'BILLING_PRICE_ALREADY_ASSIGNED'
|
|
1494
|
+
}));
|
|
1495
|
+
return;
|
|
1496
|
+
}
|
|
1497
|
+
unitAmount = typeof stripePrice.unit_amount === 'number' ? stripePrice.unit_amount : null;
|
|
1498
|
+
currency = stripePrice.currency || null;
|
|
1499
|
+
}
|
|
1500
|
+
const beforeRows = db.prepare(`
|
|
1501
|
+
SELECT plan_version_id, billing_interval, stripe_price_id, pricing_type, active, unit_amount, currency
|
|
1502
|
+
FROM billing_price_mappings
|
|
1503
|
+
WHERE tenant_id = ? AND plan_version_id = ? AND billing_interval = ?
|
|
1504
|
+
ORDER BY stripe_price_id ASC
|
|
1505
|
+
`).all(tenantId, planVersionId, interval);
|
|
1506
|
+
if (setActive) {
|
|
1507
|
+
db.prepare(`
|
|
1508
|
+
UPDATE billing_price_mappings
|
|
1509
|
+
SET active = 0, updated_at = ?
|
|
1510
|
+
WHERE tenant_id = ?
|
|
1511
|
+
AND plan_version_id = ?
|
|
1512
|
+
AND billing_interval = ?
|
|
1513
|
+
AND stripe_price_id <> ?
|
|
1514
|
+
AND active = 1
|
|
1515
|
+
`).run(now, tenantId, planVersionId, interval, targetStripePriceId);
|
|
1516
|
+
}
|
|
1517
|
+
db.prepare(`
|
|
1518
|
+
INSERT INTO billing_price_mappings (
|
|
1519
|
+
tenant_id, plan_version_id, billing_interval, stripe_price_id, pricing_type, active, unit_amount, currency, created_at, updated_at
|
|
1520
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1521
|
+
ON CONFLICT (tenant_id, plan_version_id, billing_interval, stripe_price_id) DO UPDATE SET
|
|
1522
|
+
pricing_type = excluded.pricing_type,
|
|
1523
|
+
active = excluded.active,
|
|
1524
|
+
unit_amount = excluded.unit_amount,
|
|
1525
|
+
currency = excluded.currency,
|
|
1526
|
+
updated_at = excluded.updated_at
|
|
1527
|
+
`).run(tenantId, planVersionId, interval, targetStripePriceId, pricingType, setActive ? 1 : 0, unitAmount, currency, now, now);
|
|
1528
|
+
const mapping = db.prepare(`
|
|
1529
|
+
SELECT plan_version_id, billing_interval, stripe_price_id, pricing_type, active, unit_amount, currency, created_at, updated_at
|
|
1530
|
+
FROM billing_price_mappings
|
|
1531
|
+
WHERE tenant_id = ? AND plan_version_id = ? AND billing_interval = ? AND stripe_price_id = ?
|
|
1532
|
+
LIMIT 1
|
|
1533
|
+
`).get(tenantId, planVersionId, interval, targetStripePriceId);
|
|
1534
|
+
const afterRows = db.prepare(`
|
|
1535
|
+
SELECT plan_version_id, billing_interval, stripe_price_id, pricing_type, active, unit_amount, currency
|
|
1536
|
+
FROM billing_price_mappings
|
|
1537
|
+
WHERE tenant_id = ? AND plan_version_id = ? AND billing_interval = ?
|
|
1538
|
+
ORDER BY stripe_price_id ASC
|
|
1539
|
+
`).all(tenantId, planVersionId, interval);
|
|
1540
|
+
auditAdminAction(req, 'billing-price-mapping-upsert', {
|
|
1541
|
+
planVersionId,
|
|
1542
|
+
interval,
|
|
1543
|
+
pricingType,
|
|
1544
|
+
stripePriceId: pricingType === 'free' ? null : stripePriceId,
|
|
1545
|
+
active: setActive,
|
|
1546
|
+
before: beforeRows,
|
|
1547
|
+
after: afterRows
|
|
1548
|
+
});
|
|
1549
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1550
|
+
res.end(JSON.stringify({ mapping }));
|
|
1551
|
+
}
|
|
1552
|
+
catch (error) {
|
|
1553
|
+
const statusCode = resolveErrorStatusCode(error, 400);
|
|
1554
|
+
res.writeHead(statusCode, { 'Content-Type': 'application/json' });
|
|
1555
|
+
res.end(JSON.stringify({ error: String(error?.message || 'Unable to upsert price mapping'), code: error?.code }));
|
|
1556
|
+
}
|
|
1352
1557
|
});
|
|
1353
1558
|
addRoute('POST', '/api/taskforce/admin/billing/price-mappings/deactivate', async (req, res) => {
|
|
1354
1559
|
if (!ensureCloudBilling(req, res))
|
|
@@ -1358,11 +1563,51 @@ export function registerBillingRoutes(deps) {
|
|
|
1358
1563
|
res.end(JSON.stringify({ error: 'Forbidden: system admin role required.' }));
|
|
1359
1564
|
return;
|
|
1360
1565
|
}
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1566
|
+
const body = await parseJsonBody(req);
|
|
1567
|
+
const planVersionId = String(body?.planVersionId || '').trim();
|
|
1568
|
+
const interval = parseInterval(body?.interval);
|
|
1569
|
+
const stripePriceId = String(body?.stripePriceId || '').trim();
|
|
1570
|
+
if (!planVersionId || !interval) {
|
|
1571
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
1572
|
+
res.end(JSON.stringify({ error: 'planVersionId and interval are required.', code: 'BILLING_MAPPING_TARGET_REQUIRED' }));
|
|
1573
|
+
return;
|
|
1574
|
+
}
|
|
1575
|
+
const db = core.getDatabaseAdapter();
|
|
1576
|
+
const tenantId = core.getTenantId();
|
|
1577
|
+
const now = new Date().toISOString();
|
|
1578
|
+
const beforeRows = db.prepare(`
|
|
1579
|
+
SELECT plan_version_id, billing_interval, stripe_price_id, pricing_type, active, unit_amount, currency
|
|
1580
|
+
FROM billing_price_mappings
|
|
1581
|
+
WHERE tenant_id = ? AND plan_version_id = ? AND billing_interval = ?
|
|
1582
|
+
ORDER BY stripe_price_id ASC
|
|
1583
|
+
`).all(tenantId, planVersionId, interval);
|
|
1584
|
+
const result = stripePriceId
|
|
1585
|
+
? db.prepare(`
|
|
1586
|
+
UPDATE billing_price_mappings
|
|
1587
|
+
SET active = 0, updated_at = ?
|
|
1588
|
+
WHERE tenant_id = ? AND plan_version_id = ? AND billing_interval = ? AND stripe_price_id = ?
|
|
1589
|
+
`).run(now, tenantId, planVersionId, interval, stripePriceId)
|
|
1590
|
+
: db.prepare(`
|
|
1591
|
+
UPDATE billing_price_mappings
|
|
1592
|
+
SET active = 0, updated_at = ?
|
|
1593
|
+
WHERE tenant_id = ? AND plan_version_id = ? AND billing_interval = ?
|
|
1594
|
+
`).run(now, tenantId, planVersionId, interval);
|
|
1595
|
+
const afterRows = db.prepare(`
|
|
1596
|
+
SELECT plan_version_id, billing_interval, stripe_price_id, pricing_type, active, unit_amount, currency
|
|
1597
|
+
FROM billing_price_mappings
|
|
1598
|
+
WHERE tenant_id = ? AND plan_version_id = ? AND billing_interval = ?
|
|
1599
|
+
ORDER BY stripe_price_id ASC
|
|
1600
|
+
`).all(tenantId, planVersionId, interval);
|
|
1601
|
+
auditAdminAction(req, 'billing-price-mapping-deactivate', {
|
|
1602
|
+
planVersionId,
|
|
1603
|
+
interval,
|
|
1604
|
+
stripePriceId: stripePriceId || null,
|
|
1605
|
+
changedRows: Number(result?.changes || 0),
|
|
1606
|
+
before: beforeRows,
|
|
1607
|
+
after: afterRows
|
|
1608
|
+
});
|
|
1609
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1610
|
+
res.end(JSON.stringify({ success: true, changedRows: Number(result?.changes || 0) }));
|
|
1366
1611
|
});
|
|
1367
1612
|
addRoute('GET', '/api/taskforce/admin/billing/webhook-failures', async (req, res) => {
|
|
1368
1613
|
if (!ensureCloudBilling(req, res))
|
|
@@ -1741,10 +1986,17 @@ export function registerBillingRoutes(deps) {
|
|
|
1741
1986
|
planVersionId,
|
|
1742
1987
|
interval
|
|
1743
1988
|
});
|
|
1989
|
+
const successParams = new URLSearchParams({
|
|
1990
|
+
screen: 'plans',
|
|
1991
|
+
checkout: 'success',
|
|
1992
|
+
planId: String(planId || ''),
|
|
1993
|
+
planVersionId,
|
|
1994
|
+
interval
|
|
1995
|
+
});
|
|
1744
1996
|
const session = await stripe.checkout.sessions.create({
|
|
1745
1997
|
mode: 'subscription',
|
|
1746
1998
|
line_items: [{ price: priceId, quantity: 1 }],
|
|
1747
|
-
success_url: `${baseUrl}
|
|
1999
|
+
success_url: `${baseUrl}/?${successParams.toString()}&session_id={CHECKOUT_SESSION_ID}`,
|
|
1748
2000
|
cancel_url: `${baseUrl}/?${cancelParams.toString()}`,
|
|
1749
2001
|
...(resolvedCustomerId
|
|
1750
2002
|
? { customer: resolvedCustomerId }
|
|
@@ -1863,7 +2115,10 @@ export function registerBillingRoutes(deps) {
|
|
|
1863
2115
|
}
|
|
1864
2116
|
if (resolvedUserId !== expectedUserId) {
|
|
1865
2117
|
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
1866
|
-
res.end(JSON.stringify({
|
|
2118
|
+
res.end(JSON.stringify({
|
|
2119
|
+
error: 'Checkout session does not belong to the current account.',
|
|
2120
|
+
code: 'CHECKOUT_SESSION_ACCOUNT_MISMATCH'
|
|
2121
|
+
}));
|
|
1867
2122
|
return;
|
|
1868
2123
|
}
|
|
1869
2124
|
let resolvedPlanVersionId = null;
|
|
@@ -1883,7 +2138,8 @@ export function registerBillingRoutes(deps) {
|
|
|
1883
2138
|
userId: resolvedUserId,
|
|
1884
2139
|
planVersionId: resolvedPlanVersionId,
|
|
1885
2140
|
sessionId,
|
|
1886
|
-
subscriptionId
|
|
2141
|
+
subscriptionId,
|
|
2142
|
+
source: 'checkout-confirm'
|
|
1887
2143
|
});
|
|
1888
2144
|
if (lifecycleGuard.blocked) {
|
|
1889
2145
|
res.writeHead(409, { 'Content-Type': 'application/json' });
|
|
@@ -1902,12 +2158,14 @@ export function registerBillingRoutes(deps) {
|
|
|
1902
2158
|
}));
|
|
1903
2159
|
return;
|
|
1904
2160
|
}
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
2161
|
+
if (!resolvedPlanVersionId) {
|
|
2162
|
+
res.writeHead(409, { 'Content-Type': 'application/json' });
|
|
2163
|
+
res.end(JSON.stringify({
|
|
2164
|
+
error: 'Checkout completed, but the purchased plan could not be resolved yet.',
|
|
2165
|
+
code: 'CHECKOUT_PLAN_VERSION_UNRESOLVED'
|
|
2166
|
+
}));
|
|
2167
|
+
return;
|
|
2168
|
+
}
|
|
1911
2169
|
const snapshot = applySubscriptionSnapshot(core, {
|
|
1912
2170
|
userId: resolvedUserId,
|
|
1913
2171
|
subscription,
|
|
@@ -2165,7 +2423,8 @@ export function registerBillingRoutes(deps) {
|
|
|
2165
2423
|
const lifecycleGuard = resolveCheckoutLifecycleGuard(core, {
|
|
2166
2424
|
userId,
|
|
2167
2425
|
planVersionId,
|
|
2168
|
-
subscriptionId: String(subscription.id || '').trim()
|
|
2426
|
+
subscriptionId: String(subscription.id || '').trim(),
|
|
2427
|
+
source: 'stripe-webhook'
|
|
2169
2428
|
});
|
|
2170
2429
|
if (lifecycleGuard.blocked) {
|
|
2171
2430
|
recordWebhookFailure(core, {
|
|
@@ -2210,7 +2469,8 @@ export function registerBillingRoutes(deps) {
|
|
|
2210
2469
|
const lifecycleGuard = resolveCheckoutLifecycleGuard(core, {
|
|
2211
2470
|
userId,
|
|
2212
2471
|
planVersionId,
|
|
2213
|
-
subscriptionId
|
|
2472
|
+
subscriptionId,
|
|
2473
|
+
source: 'stripe-webhook'
|
|
2214
2474
|
});
|
|
2215
2475
|
if (lifecycleGuard.blocked) {
|
|
2216
2476
|
recordWebhookFailure(core, {
|
|
@@ -2292,6 +2552,15 @@ export function registerBillingRoutes(deps) {
|
|
|
2292
2552
|
try {
|
|
2293
2553
|
const db = core.getDatabaseAdapter();
|
|
2294
2554
|
const tenantId = core.getTenantId();
|
|
2555
|
+
const environment = requestPricingEnvironment(req);
|
|
2556
|
+
const cacheKey = `${tenantId}:${environment}`;
|
|
2557
|
+
const cacheVersion = buildPublicPricingCacheVersion(core);
|
|
2558
|
+
const cached = publicPricingCache.get(cacheKey);
|
|
2559
|
+
if (cached && cached.version === cacheVersion && cached.expiresAt > Date.now()) {
|
|
2560
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
2561
|
+
res.end(JSON.stringify(cached.payload));
|
|
2562
|
+
return;
|
|
2563
|
+
}
|
|
2295
2564
|
// Get all active plans
|
|
2296
2565
|
const activePlans = core.listPlanCatalog().filter(p => p.status === 'active');
|
|
2297
2566
|
// Get all active price mappings with their amounts
|
|
@@ -2300,6 +2569,17 @@ export function registerBillingRoutes(deps) {
|
|
|
2300
2569
|
FROM billing_price_mappings
|
|
2301
2570
|
WHERE tenant_id = ? AND active = 1
|
|
2302
2571
|
`).all(tenantId);
|
|
2572
|
+
const pricingByVersionAndInterval = new Map();
|
|
2573
|
+
for (const price of activePrices) {
|
|
2574
|
+
const planVersionId = String(price.plan_version_id || '').trim();
|
|
2575
|
+
const interval = parseInterval(price.billing_interval);
|
|
2576
|
+
if (!planVersionId || !interval)
|
|
2577
|
+
continue;
|
|
2578
|
+
pricingByVersionAndInterval.set(`${planVersionId}:${interval}`, price);
|
|
2579
|
+
}
|
|
2580
|
+
const featureCatalog = core.listPlanFeatureCatalog();
|
|
2581
|
+
const featureCatalogByKey = new Map(featureCatalog.map((feature) => [feature.featureKey, feature]));
|
|
2582
|
+
const featureCatalogIndexByKey = new Map(featureCatalog.map((feature, index) => [feature.featureKey, index]));
|
|
2303
2583
|
const plansPayload = activePlans.map((plan) => {
|
|
2304
2584
|
const versions = core.listPlanVersions(plan.planId);
|
|
2305
2585
|
// Prefer default, else fallback to highest version number
|
|
@@ -2307,11 +2587,12 @@ export function registerBillingRoutes(deps) {
|
|
|
2307
2587
|
if (!defaultVersion) {
|
|
2308
2588
|
return null; // Skip if no valid version exists (edge case)
|
|
2309
2589
|
}
|
|
2310
|
-
const monthPrice =
|
|
2311
|
-
const yearPrice =
|
|
2590
|
+
const monthPrice = pricingByVersionAndInterval.get(`${defaultVersion.planVersionId}:month`);
|
|
2591
|
+
const yearPrice = pricingByVersionAndInterval.get(`${defaultVersion.planVersionId}:year`);
|
|
2312
2592
|
return {
|
|
2313
2593
|
planId: plan.planId,
|
|
2314
2594
|
displayName: plan.displayName,
|
|
2595
|
+
description: plan.description ?? null,
|
|
2315
2596
|
status: plan.status,
|
|
2316
2597
|
defaultVersion: {
|
|
2317
2598
|
planVersionId: defaultVersion.planVersionId,
|
|
@@ -2320,9 +2601,48 @@ export function registerBillingRoutes(deps) {
|
|
|
2320
2601
|
trialDays: defaultVersion.trialDays,
|
|
2321
2602
|
metadata: defaultVersion.metadata
|
|
2322
2603
|
},
|
|
2323
|
-
features: Object.values(defaultVersion.features || {})
|
|
2604
|
+
features: Object.values(defaultVersion.features || {})
|
|
2605
|
+
.reduce((acc, feature) => {
|
|
2324
2606
|
const access = String(feature?.access || '').trim().toLowerCase();
|
|
2325
|
-
|
|
2607
|
+
if (access === 'disabled') {
|
|
2608
|
+
return acc;
|
|
2609
|
+
}
|
|
2610
|
+
const featureKey = String(feature?.featureKey || '').trim();
|
|
2611
|
+
if (!featureKey || INTERNAL_PUBLIC_PRICING_FEATURE_KEYS.has(featureKey)) {
|
|
2612
|
+
return acc;
|
|
2613
|
+
}
|
|
2614
|
+
const catalogEntry = featureCatalogByKey.get(featureKey);
|
|
2615
|
+
acc.push({
|
|
2616
|
+
featureKey,
|
|
2617
|
+
access,
|
|
2618
|
+
config: feature?.config && typeof feature.config === 'object'
|
|
2619
|
+
? feature.config
|
|
2620
|
+
: {},
|
|
2621
|
+
label: catalogEntry?.label ?? null,
|
|
2622
|
+
description: catalogEntry?.description ?? null,
|
|
2623
|
+
publicLabel: catalogEntry?.publicLabel ?? null,
|
|
2624
|
+
publicDescription: catalogEntry?.publicDescription ?? null
|
|
2625
|
+
});
|
|
2626
|
+
return acc;
|
|
2627
|
+
}, [])
|
|
2628
|
+
.sort((a, b) => {
|
|
2629
|
+
const orderA = featureCatalogByKey.get(a.featureKey)?.publicDisplayOrder;
|
|
2630
|
+
const orderB = featureCatalogByKey.get(b.featureKey)?.publicDisplayOrder;
|
|
2631
|
+
const normalizedOrderA = typeof orderA === 'number' && Number.isFinite(orderA)
|
|
2632
|
+
? orderA
|
|
2633
|
+
: Number.MAX_SAFE_INTEGER;
|
|
2634
|
+
const normalizedOrderB = typeof orderB === 'number' && Number.isFinite(orderB)
|
|
2635
|
+
? orderB
|
|
2636
|
+
: Number.MAX_SAFE_INTEGER;
|
|
2637
|
+
if (normalizedOrderA !== normalizedOrderB) {
|
|
2638
|
+
return normalizedOrderA - normalizedOrderB;
|
|
2639
|
+
}
|
|
2640
|
+
const catalogIndexA = featureCatalogIndexByKey.get(a.featureKey) ?? Number.MAX_SAFE_INTEGER;
|
|
2641
|
+
const catalogIndexB = featureCatalogIndexByKey.get(b.featureKey) ?? Number.MAX_SAFE_INTEGER;
|
|
2642
|
+
if (catalogIndexA !== catalogIndexB) {
|
|
2643
|
+
return catalogIndexA - catalogIndexB;
|
|
2644
|
+
}
|
|
2645
|
+
return a.featureKey.localeCompare(b.featureKey);
|
|
2326
2646
|
}),
|
|
2327
2647
|
pricing: {
|
|
2328
2648
|
month: monthPrice ? {
|
|
@@ -2342,15 +2662,21 @@ export function registerBillingRoutes(deps) {
|
|
|
2342
2662
|
}
|
|
2343
2663
|
};
|
|
2344
2664
|
}).filter(Boolean);
|
|
2345
|
-
|
|
2346
|
-
res.end(JSON.stringify({
|
|
2665
|
+
const payload = {
|
|
2347
2666
|
asOf: new Date().toISOString(),
|
|
2348
2667
|
pricingSource: {
|
|
2349
|
-
environment
|
|
2668
|
+
environment,
|
|
2350
2669
|
runtimeMode: 'cloud'
|
|
2351
2670
|
},
|
|
2352
2671
|
plans: plansPayload
|
|
2353
|
-
}
|
|
2672
|
+
};
|
|
2673
|
+
publicPricingCache.set(cacheKey, {
|
|
2674
|
+
payload,
|
|
2675
|
+
version: cacheVersion,
|
|
2676
|
+
expiresAt: Date.now() + PUBLIC_PRICING_CACHE_TTL_MS
|
|
2677
|
+
});
|
|
2678
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
2679
|
+
res.end(JSON.stringify(payload));
|
|
2354
2680
|
}
|
|
2355
2681
|
catch (error) {
|
|
2356
2682
|
const statusCode = resolveErrorStatusCode(error, 500);
|