@taskforcehq/taskforce 0.3.170 → 0.3.301

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.
Files changed (698) hide show
  1. package/README.md +71 -21
  2. package/dist/Taskforce.module.css +3394 -5981
  3. package/dist/TaskforceCore.CategoryFallback.test.js +5 -2
  4. package/dist/TaskforceCore.filter-persistence.test.js +402 -0
  5. package/dist/TaskforceCore.js +388 -71
  6. package/dist/TaskforceCore.routing.test.js +311 -0
  7. package/dist/TaskforceCore.test.js +3344 -615
  8. package/dist/TaskforceWidget.js +3 -2
  9. package/dist/__tests__/localizationCatalogs.test.js +48 -31
  10. package/dist/__tests__/workflowSimulation.test.js +2 -2
  11. package/dist/annotatedAttachments/AnnotatedAttachmentSessionStore.d.ts +60 -0
  12. package/dist/annotatedAttachments/AnnotatedAttachmentSessionStore.js +324 -0
  13. package/dist/annotatedAttachments/AnnotatedAttachmentSessionStore.test.js +278 -0
  14. package/dist/annotatedAttachments/payload.d.ts +2 -0
  15. package/dist/annotatedAttachments/payload.js +54 -0
  16. package/dist/annotatedAttachments/payload.test.js +67 -0
  17. package/dist/annotatedAttachments/service.d.ts +49 -0
  18. package/dist/annotatedAttachments/service.js +121 -0
  19. package/dist/annotatedAttachments/types.d.ts +86 -0
  20. package/dist/cli.js +74 -25
  21. package/dist/compat/deploymentCompat.d.ts +12 -0
  22. package/dist/compat/deploymentCompat.js +43 -0
  23. package/dist/compat/workspaceSyncCompat.d.ts +6 -0
  24. package/dist/compat/workspaceSyncCompat.js +79 -0
  25. package/dist/compat/workspaceSyncCompat.test.js +87 -0
  26. package/dist/components/features/AgentsModule.d.ts +5 -0
  27. package/dist/components/features/AgentsModule.js +131 -0
  28. package/dist/components/features/AgentsModule.test.js +102 -0
  29. package/dist/components/features/AnnotatedAttachmentWorkspace.d.ts +24 -0
  30. package/dist/components/features/AnnotatedAttachmentWorkspace.js +1942 -0
  31. package/dist/components/features/AnnotatedAttachmentWorkspace.test.js +2831 -0
  32. package/dist/components/features/DocumentGenerateModal.js +8 -16
  33. package/dist/components/features/DocumentIndex.d.ts +3 -4
  34. package/dist/components/features/DocumentIndex.js +22 -10
  35. package/dist/components/features/DocumentViewer.d.ts +6 -2
  36. package/dist/components/features/DocumentViewer.js +1000 -18
  37. package/dist/components/features/DocumentWorkspace.d.ts +40 -2
  38. package/dist/components/features/DocumentWorkspace.js +96 -16
  39. package/dist/components/features/DocumentWorkspace.test.js +1202 -0
  40. package/dist/components/features/InitiativesModule.d.ts +22 -0
  41. package/dist/components/features/InitiativesModule.js +33 -0
  42. package/dist/components/features/TaskSettings.d.ts +41 -32
  43. package/dist/components/features/TaskSettings.js +798 -313
  44. package/dist/components/features/TaskSettings.test.js +1221 -47
  45. package/dist/components/features/WorkflowsModule.d.ts +27 -0
  46. package/dist/components/features/WorkflowsModule.js +21 -0
  47. package/dist/components/features/documentWorkspaceFilters.d.ts +10 -0
  48. package/dist/components/features/documentWorkspaceFilters.js +11 -0
  49. package/dist/components/features/settings/CategoryManager.d.ts +2 -0
  50. package/dist/components/features/settings/CategoryManager.js +15 -8
  51. package/dist/components/features/settings/PriorityManager.d.ts +18 -0
  52. package/dist/components/features/settings/PriorityManager.js +51 -0
  53. package/dist/components/features/settings/TaxonomyEditor.d.ts +2 -1
  54. package/dist/components/features/settings/TaxonomyEditor.js +41 -18
  55. package/dist/components/features/settings/TaxonomyLibraryManager.d.ts +26 -0
  56. package/dist/components/features/settings/TaxonomyLibraryManager.js +301 -0
  57. package/dist/components/features/settings/TaxonomyLibraryManager.test.js +141 -0
  58. package/dist/components/features/settings/TaxonomyManager.d.ts +35 -12
  59. package/dist/components/features/settings/TaxonomyManager.js +59 -27
  60. package/dist/components/features/settings/TypeManager.d.ts +11 -0
  61. package/dist/components/features/settings/TypeManager.js +48 -11
  62. package/dist/components/features/settings/{WorkflowEditorPrototype.d.ts → WorkflowEditor.d.ts} +2 -2
  63. package/dist/components/features/settings/{WorkflowEditorPrototype.js → WorkflowEditor.js} +3 -3
  64. package/dist/components/task/TaskActionHeader.d.ts +12 -2
  65. package/dist/components/task/TaskActionHeader.js +64 -8
  66. package/dist/components/task/TaskActionHeader.test.js +73 -0
  67. package/dist/components/task/TaskCard.d.ts +14 -16
  68. package/dist/components/task/TaskCard.js +144 -174
  69. package/dist/components/task/TaskCard.test.js +416 -64
  70. package/dist/components/task/TaskContextUpload.d.ts +6 -5
  71. package/dist/components/task/TaskContextUpload.js +199 -64
  72. package/dist/components/task/TaskContextUpload.test.js +331 -14
  73. package/dist/components/task/TaskForm.d.ts +23 -29
  74. package/dist/components/task/TaskForm.js +348 -169
  75. package/dist/components/task/TaskForm.test.js +419 -23
  76. package/dist/components/task/TaskKanban.d.ts +20 -21
  77. package/dist/components/task/TaskKanban.js +176 -316
  78. package/dist/components/task/TaskKanban.planning-drop.test.js +93 -0
  79. package/dist/components/task/TaskList.d.ts +22 -11
  80. package/dist/components/task/TaskList.js +98 -12
  81. package/dist/components/task/TaskList.test.js +206 -7
  82. package/dist/components/task/TaskReferenceBadge.d.ts +12 -0
  83. package/dist/components/task/TaskReferenceBadge.js +5 -0
  84. package/dist/components/task/TaskStatusActions.d.ts +12 -0
  85. package/dist/components/task/TaskStatusActions.js +18 -0
  86. package/dist/components/task/TaskStatusActions.test.js +45 -0
  87. package/dist/components/ui/ImageReferenceBadge.d.ts +12 -0
  88. package/dist/components/ui/ImageReferenceBadge.js +5 -0
  89. package/dist/components/ui/LevelSelector.d.ts +2 -1
  90. package/dist/components/ui/LevelSelector.js +2 -2
  91. package/dist/components/ui/Markdown.d.ts +1 -2
  92. package/dist/components/ui/Markdown.js +2 -13
  93. package/dist/components/ui/Modal.d.ts +3 -2
  94. package/dist/components/ui/Modal.js +4 -3
  95. package/dist/components/ui/MultiSelectFilter.d.ts +3 -1
  96. package/dist/components/ui/MultiSelectFilter.js +2 -2
  97. package/dist/components/ui/ReferenceBadgeButton.d.ts +13 -0
  98. package/dist/components/ui/ReferenceBadgeButton.js +6 -0
  99. package/dist/components/ui/TaxonomyDropdown.d.ts +7 -1
  100. package/dist/components/ui/TaxonomyDropdown.js +58 -23
  101. package/dist/components/ui/TopNoticeLayer.d.ts +7 -0
  102. package/dist/components/ui/TopNoticeLayer.js +14 -0
  103. package/dist/components/ui/TopNoticeLayer.test.js +25 -0
  104. package/dist/components/views/AppShellHeader.d.ts +13 -0
  105. package/dist/components/views/AppShellHeader.js +19 -0
  106. package/dist/components/views/AppShellHeader.test.js +21 -0
  107. package/dist/components/views/PlanComparisonPage.d.ts +73 -0
  108. package/dist/components/views/PlanComparisonPage.js +196 -0
  109. package/dist/components/views/PlanComparisonPage.test.js +251 -0
  110. package/dist/components/views/PlansPage.d.ts +14 -0
  111. package/dist/components/views/PlansPage.js +355 -0
  112. package/dist/components/views/StandaloneLayout.d.ts +2 -2
  113. package/dist/components/views/StandaloneLayout.js +2227 -1188
  114. package/dist/components/views/WidgetView.d.ts +2 -2
  115. package/dist/components/views/WidgetView.js +149 -43
  116. package/dist/components/views/chrome/WorkspaceChromeSections.d.ts +57 -0
  117. package/dist/components/views/chrome/WorkspaceChromeSections.js +38 -0
  118. package/dist/components/views/chrome/WorkspaceChromeSections.test.js +54 -0
  119. package/dist/components/views/chrome/workspaceChrome.d.ts +31 -0
  120. package/dist/components/views/chrome/workspaceChrome.js +108 -0
  121. package/dist/components/views/chrome/workspaceChrome.test.js +52 -0
  122. package/dist/components/views/panels/DocumentFilterToolbar.d.ts +10 -0
  123. package/dist/components/views/panels/DocumentFilterToolbar.js +7 -0
  124. package/dist/components/views/panels/DocumentFilterToolbar.test.js +19 -0
  125. package/dist/components/views/panels/FilterToolbar.d.ts +68 -0
  126. package/dist/components/views/panels/FilterToolbar.js +21 -0
  127. package/dist/components/views/panels/FilterToolbar.test.d.ts +1 -0
  128. package/dist/components/views/panels/FilterToolbar.test.js +45 -0
  129. package/dist/components/views/panels/FilterToolbarControls.d.ts +47 -0
  130. package/dist/components/views/panels/FilterToolbarControls.js +61 -0
  131. package/dist/components/views/panels/PlanningDrawer.d.ts +115 -0
  132. package/dist/components/views/panels/PlanningDrawer.js +220 -0
  133. package/dist/components/views/panels/ScheduleSidebar.d.ts +52 -0
  134. package/dist/components/views/panels/ScheduleSidebar.js +127 -0
  135. package/dist/components/views/panels/scheduleUtils.d.ts +14 -0
  136. package/dist/components/views/panels/scheduleUtils.js +79 -0
  137. package/dist/components/views/teamManagementAccess.d.ts +6 -0
  138. package/dist/components/views/teamManagementAccess.js +18 -0
  139. package/dist/components/views/teamManagementAccess.test.d.ts +1 -0
  140. package/dist/components/views/teamManagementAccess.test.js +44 -0
  141. package/dist/config/clientDeployment.d.ts +2 -0
  142. package/dist/config/clientDeployment.js +13 -0
  143. package/dist/config/cloudEnvironment.d.ts +15 -0
  144. package/dist/config/cloudEnvironment.js +60 -0
  145. package/dist/config/deployment.d.ts +20 -0
  146. package/dist/config/deployment.js +67 -0
  147. package/dist/config/deployment.test.d.ts +1 -0
  148. package/dist/config/deployment.test.js +88 -0
  149. package/dist/config/envSchema.js +36 -8
  150. package/dist/config/envSchema.test.js +28 -0
  151. package/dist/core/AiProfileService.d.ts +66 -0
  152. package/dist/core/AiProfileService.js +320 -0
  153. package/dist/core/AiProfileService.test.d.ts +1 -0
  154. package/dist/core/AiProfileService.test.js +164 -0
  155. package/dist/core/AiProfiles.test.d.ts +1 -0
  156. package/dist/core/AiProfiles.test.js +449 -0
  157. package/dist/core/AttachmentLinkService.d.ts +15 -0
  158. package/dist/core/AttachmentLinkService.js +157 -0
  159. package/dist/core/AuthTokenService.d.ts +44 -0
  160. package/dist/core/AuthTokenService.js +207 -0
  161. package/dist/core/CreatedBy.test.js +134 -6
  162. package/dist/core/DeletedTaskLifecycleService.d.ts +33 -0
  163. package/dist/core/DeletedTaskLifecycleService.js +158 -0
  164. package/dist/core/DeletedTaskTrash.test.d.ts +1 -0
  165. package/dist/core/DeletedTaskTrash.test.js +104 -0
  166. package/dist/core/EntitlementsPolicy.test.js +78 -5
  167. package/dist/core/GlobalSettingsService.d.ts +16 -0
  168. package/dist/core/GlobalSettingsService.js +370 -0
  169. package/dist/core/JsonBackupRuntime.test.d.ts +1 -0
  170. package/dist/core/JsonBackupRuntime.test.js +155 -0
  171. package/dist/core/McpTokenService.d.ts +118 -0
  172. package/dist/core/McpTokenService.js +921 -0
  173. package/dist/core/PlanEntitlementService.d.ts +201 -0
  174. package/dist/core/PlanEntitlementService.js +1442 -0
  175. package/dist/core/PlanVersionPolicy.test.d.ts +1 -0
  176. package/dist/core/PlanVersionPolicy.test.js +178 -0
  177. package/dist/core/PlanningEntities.test.d.ts +1 -0
  178. package/dist/core/PlanningEntities.test.js +95 -0
  179. package/dist/core/ScheduleFields.test.js +15 -19
  180. package/dist/core/SignupMonetizationSettings.test.d.ts +1 -0
  181. package/dist/core/SignupMonetizationSettings.test.js +159 -0
  182. package/dist/core/Stats.test.d.ts +1 -0
  183. package/dist/core/Stats.test.js +36 -0
  184. package/dist/core/SyncReconciliationService.d.ts +93 -0
  185. package/dist/core/SyncReconciliationService.js +406 -0
  186. package/dist/core/SystemAdmin.test.js +578 -13
  187. package/dist/core/TaskActivityService.d.ts +21 -0
  188. package/dist/core/TaskActivityService.js +130 -0
  189. package/dist/core/TaskAttachmentsCanonical.test.d.ts +1 -0
  190. package/dist/core/TaskAttachmentsCanonical.test.js +306 -0
  191. package/dist/core/TaskAuditTimeline.test.d.ts +1 -0
  192. package/dist/core/TaskAuditTimeline.test.js +132 -0
  193. package/dist/core/TaskTaxonomyValidation.test.js +338 -15
  194. package/dist/core/Taskforce.d.ts +930 -183
  195. package/dist/core/Taskforce.ids.test.js +252 -0
  196. package/dist/core/Taskforce.js +3848 -2532
  197. package/dist/core/Taskforce.mcpAuth.test.d.ts +1 -0
  198. package/dist/core/Taskforce.mcpAuth.test.js +343 -0
  199. package/dist/core/Taskforce.realtimeMutation.test.js +57 -2
  200. package/dist/core/TaxonomySettingsService.d.ts +18 -0
  201. package/dist/core/TaxonomySettingsService.js +162 -0
  202. package/dist/core/WorkspaceDelete.test.js +32 -13
  203. package/dist/core/WorkspaceLifecycleService.d.ts +11 -0
  204. package/dist/core/WorkspaceLifecycleService.js +88 -0
  205. package/dist/core/WorkspacePermissions.test.js +95 -0
  206. package/dist/core/WorkspacePlanMode.test.js +114 -1
  207. package/dist/core/WorkspacePreferenceService.d.ts +41 -0
  208. package/dist/core/WorkspacePreferenceService.js +246 -0
  209. package/dist/core/WorkspaceSyncSettings.test.d.ts +1 -0
  210. package/dist/core/WorkspaceSyncSettings.test.js +65 -0
  211. package/dist/core/__tests__/TaskAttachmentsCanonical.test.d.ts +1 -0
  212. package/dist/core/__tests__/TaskAttachmentsCanonical.test.js +212 -0
  213. package/dist/core/shared.d.ts +36 -0
  214. package/dist/core/shared.js +128 -0
  215. package/dist/core/types.d.ts +614 -0
  216. package/dist/core/types.js +27 -0
  217. package/dist/documentReviews/DocumentReviewCommentStore.d.ts +56 -0
  218. package/dist/documentReviews/DocumentReviewCommentStore.js +230 -0
  219. package/dist/documentReviews/DocumentReviewSessionStore.d.ts +52 -0
  220. package/dist/documentReviews/DocumentReviewSessionStore.js +193 -0
  221. package/dist/documentReviews/DocumentReviewSessionStore.test.d.ts +1 -0
  222. package/dist/documentReviews/DocumentReviewSessionStore.test.js +127 -0
  223. package/dist/documentReviews/service.d.ts +68 -0
  224. package/dist/documentReviews/service.js +210 -0
  225. package/dist/documentReviews/types.d.ts +58 -0
  226. package/dist/documentReviews/types.js +1 -0
  227. package/dist/hooks/auth/useAuthGuardPolicy.d.ts +2 -0
  228. package/dist/hooks/auth/useAuthGuardPolicy.js +1 -0
  229. package/dist/hooks/tasks/useCurrentTaskRelations.d.ts +15 -0
  230. package/dist/hooks/tasks/useCurrentTaskRelations.js +37 -0
  231. package/dist/hooks/tasks/useTaskActions.d.ts +3 -0
  232. package/dist/hooks/tasks/useTaskActions.js +2 -0
  233. package/dist/hooks/tasks/useTaskEditorNavigation.d.ts +65 -0
  234. package/dist/hooks/tasks/useTaskEditorNavigation.js +235 -0
  235. package/dist/hooks/tasks/useTaskFilters.d.ts +2 -0
  236. package/dist/hooks/tasks/useTaskFilters.js +2 -0
  237. package/dist/hooks/tasks/useTaskFormActions.d.ts +58 -0
  238. package/dist/hooks/tasks/useTaskFormActions.js +365 -0
  239. package/dist/hooks/tasks/useTaskRelationships.d.ts +16 -0
  240. package/dist/hooks/tasks/useTaskRelationships.js +64 -0
  241. package/dist/hooks/ui/useResourceExport.d.ts +19 -0
  242. package/dist/hooks/ui/useResourceExport.js +84 -0
  243. package/dist/hooks/ui/useSettingsModel.d.ts +203 -0
  244. package/dist/hooks/ui/useSettingsModel.js +125 -0
  245. package/dist/hooks/ui/useSettingsPersistence.d.ts +36 -0
  246. package/dist/hooks/ui/useSettingsPersistence.js +254 -0
  247. package/dist/hooks/ui/useSettingsTaxonomyManagement.d.ts +119 -0
  248. package/dist/hooks/ui/useSettingsTaxonomyManagement.js +536 -0
  249. package/dist/hooks/ui/useUiNotice.d.ts +1 -0
  250. package/dist/hooks/ui/useUiNotice.js +1 -0
  251. package/dist/hooks/ui/useWorkflowTemplates.d.ts +37 -0
  252. package/dist/hooks/ui/useWorkflowTemplates.js +111 -0
  253. package/dist/hooks/ui/useZenMode.d.ts +5 -0
  254. package/dist/hooks/ui/useZenMode.js +32 -0
  255. package/dist/hooks/useFilterSort.d.ts +60 -0
  256. package/dist/hooks/useFilterSort.js +362 -0
  257. package/dist/hooks/useFilterSort.test.d.ts +1 -0
  258. package/dist/hooks/useFilterSort.test.js +333 -0
  259. package/dist/hooks/useRealtimeSync.d.ts +1 -0
  260. package/dist/hooks/useRealtimeSync.js +24 -9
  261. package/dist/hooks/useRealtimeSync.test.js +28 -1
  262. package/dist/hooks/useSyncOrchestrator.aiProfiles.test.d.ts +1 -0
  263. package/dist/hooks/useSyncOrchestrator.aiProfiles.test.js +34 -0
  264. package/dist/hooks/useSyncOrchestrator.d.ts +65 -103
  265. package/dist/hooks/useSyncOrchestrator.js +1538 -670
  266. package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +700 -174
  267. package/dist/hooks/useTaskData.d.ts +35 -0
  268. package/dist/hooks/useTaskData.js +243 -0
  269. package/dist/hooks/useTaskData.test.d.ts +1 -0
  270. package/dist/hooks/useTaskData.test.js +180 -0
  271. package/dist/hooks/useTaskMutations.d.ts +46 -0
  272. package/dist/hooks/useTaskMutations.js +472 -0
  273. package/dist/hooks/useTaskMutations.test.d.ts +1 -0
  274. package/dist/hooks/useTaskMutations.test.js +80 -0
  275. package/dist/hooks/useTaskforce.contract.test.d.ts +1 -0
  276. package/dist/hooks/useTaskforce.contract.test.js +7 -0
  277. package/dist/hooks/useTaskforce.d.ts +260 -295
  278. package/dist/hooks/useTaskforce.js +1954 -2623
  279. package/dist/hooks/useTaskforce.priority-filters.test.js +6 -11
  280. package/dist/hooks/useTaskforce.sync-behavior.test.d.ts +1 -0
  281. package/dist/hooks/useTaskforce.sync-behavior.test.js +1120 -0
  282. package/dist/hooks/useUiNotice.d.ts +16 -0
  283. package/dist/hooks/useUiNotice.js +49 -0
  284. package/dist/hooks/useWorkspaceSyncController.d.ts +23 -46
  285. package/dist/hooks/useWorkspaceSyncController.js +280 -461
  286. package/dist/hooks/useWorkspaceSyncController.test.js +106 -199
  287. package/dist/hooks/workspace/useTaskforceApiRouting.d.ts +8 -0
  288. package/dist/hooks/workspace/useTaskforceApiRouting.js +91 -0
  289. package/dist/hooks/workspace/useTaskforceApiRouting.test.d.ts +1 -0
  290. package/dist/hooks/workspace/useTaskforceApiRouting.test.js +60 -0
  291. package/dist/hooks/workspace/useWorkspaceSyncController.d.ts +1 -0
  292. package/dist/hooks/workspace/useWorkspaceSyncController.js +1 -0
  293. package/dist/hooks/workspace/workspaceLocalState.d.ts +30 -0
  294. package/dist/hooks/workspace/workspaceLocalState.js +101 -0
  295. package/dist/localization/locales/en-US.d.ts +1 -8
  296. package/dist/localization/locales/en-US.js +4 -11
  297. package/dist/localization/locales/es-419.js +2 -9
  298. package/dist/localization/locales/pt-BR.js +2 -9
  299. package/dist/localization/localizationCatalogs.test.d.ts +1 -0
  300. package/dist/localization/localizationCatalogs.test.js +37 -0
  301. package/dist/main.d.ts +1 -0
  302. package/dist/main.js +9 -15
  303. package/dist/mcp/clientIntegrations.d.ts +33 -0
  304. package/dist/mcp/clientIntegrations.js +199 -0
  305. package/dist/mcp/clientIntegrations.test.d.ts +1 -0
  306. package/dist/mcp/clientIntegrations.test.js +110 -0
  307. package/dist/mcp/index.d.ts +2 -0
  308. package/dist/mcp/index.js +2 -0
  309. package/dist/mcp/optionResolution.test.js +6 -2
  310. package/dist/mcp/runtime.d.ts +120 -0
  311. package/dist/mcp/runtime.js +5668 -0
  312. package/dist/mcp/runtime.test.d.ts +1 -0
  313. package/dist/mcp/runtime.test.js +3299 -0
  314. package/dist/mcp/server.js +2 -1793
  315. package/dist/mcp/structuredCommentValidation.test.d.ts +1 -0
  316. package/dist/{__tests__ → mcp}/structuredCommentValidation.test.js +1 -1
  317. package/dist/mcp/structuredDocValidation.js +1 -2
  318. package/dist/mcp/structuredDocValidation.test.d.ts +1 -0
  319. package/dist/{__tests__ → mcp}/structuredDocValidation.test.js +9 -4
  320. package/dist/mcp/toolRegistry.d.ts +570 -0
  321. package/dist/mcp/toolRegistry.js +90 -0
  322. package/dist/mcp/toolRegistry.test.d.ts +1 -0
  323. package/dist/mcp/toolRegistry.test.js +30 -0
  324. package/dist/migrations/billingSchemaParity.test.js +111 -0
  325. package/dist/migrations/taskSchemaMigrations.d.ts +5 -0
  326. package/dist/migrations/taskSchemaMigrations.js +681 -39
  327. package/dist/migrations/taskSchemaMigrations.test.d.ts +1 -0
  328. package/dist/migrations/taskSchemaMigrations.test.js +210 -0
  329. package/dist/plugins/vite.js +13 -7
  330. package/dist/public/apple-touch-icon.png +0 -0
  331. package/dist/public/archive/phoenix/apple-touch-icon.png +0 -0
  332. package/dist/public/archive/phoenix/favicon.ico +0 -0
  333. package/dist/public/archive/phoenix/site.webmanifest +1 -0
  334. package/dist/public/favicon-96x96.png +0 -0
  335. package/dist/public/favicon.ico +0 -0
  336. package/dist/public/favicon.svg +17 -0
  337. package/dist/public/site.webmanifest +21 -1
  338. package/dist/public/web-app-manifest-192x192.png +0 -0
  339. package/dist/public/web-app-manifest-512x512.png +0 -0
  340. package/dist/releaseVersion.test.d.ts +1 -0
  341. package/dist/{__tests__/releaseVersion.test.js → releaseVersion.test.js} +1 -1
  342. package/dist/resources/templates/WORKFLOW_V2_POLICY.md +1 -1
  343. package/dist/resources/templates/workflow-sources/workflowDocs.mjs +62 -52
  344. package/dist/resources/templates/workflows/collaborate.yaml +11 -5
  345. package/dist/resources/templates/workflows/do.yaml +8 -2
  346. package/dist/resources/templates/workflows/evaluate.yaml +13 -7
  347. package/dist/resources/templates/workflows/execute.yaml +40 -23
  348. package/dist/resources/templates/workflows/plan.yaml +15 -8
  349. package/dist/resources/templates/workflows/review.yaml +14 -8
  350. package/dist/runtime/appGateDefinitions.d.ts +32 -0
  351. package/dist/runtime/appGateDefinitions.js +32 -0
  352. package/dist/runtime/appGates.d.ts +29 -0
  353. package/dist/runtime/appGates.js +53 -0
  354. package/dist/runtime/appGates.test.d.ts +1 -0
  355. package/dist/runtime/appGates.test.js +42 -0
  356. package/dist/runtime/migrationFlags.d.ts +1 -0
  357. package/dist/runtime/migrationFlags.js +1 -0
  358. package/dist/server/annotatedAttachmentsRoutes.test.d.ts +1 -0
  359. package/dist/server/annotatedAttachmentsRoutes.test.js +473 -0
  360. package/dist/server/auth.js +14 -2
  361. package/dist/server/auth.test.js +21 -1
  362. package/dist/server/cors.js +11 -5
  363. package/dist/server/cors.test.js +6 -0
  364. package/dist/server/documentReviewRoutes.test.d.ts +1 -0
  365. package/dist/server/documentReviewRoutes.test.js +157 -0
  366. package/dist/server/index.cookieProxy.test.js +46 -1
  367. package/dist/server/index.d.ts +20 -0
  368. package/dist/server/index.js +168 -41
  369. package/dist/server/index.rateLimit.test.js +6 -0
  370. package/dist/server/index.test.js +133 -2
  371. package/dist/server/mockStripe.d.ts +72 -0
  372. package/dist/server/mockStripe.js +357 -0
  373. package/dist/server/realtimeSyncWs.js +4 -0
  374. package/dist/server/routes/admin.d.ts +114 -0
  375. package/dist/server/routes/admin.js +2226 -0
  376. package/dist/server/routes/annotatedAttachments.d.ts +24 -0
  377. package/dist/server/routes/annotatedAttachments.js +235 -0
  378. package/dist/server/routes/auth.d.ts +61 -0
  379. package/dist/server/routes/auth.js +1296 -0
  380. package/dist/server/{routes.billing.d.ts → routes/billing.d.ts} +3 -2
  381. package/dist/server/routes/billing.js +2001 -0
  382. package/dist/server/routes/billing.test.d.ts +1 -0
  383. package/dist/server/routes/billing.test.js +1631 -0
  384. package/dist/server/routes/documentReviews.d.ts +11 -0
  385. package/dist/server/routes/documentReviews.js +191 -0
  386. package/dist/server/routes/documents.d.ts +118 -0
  387. package/dist/server/routes/documents.js +1113 -0
  388. package/dist/server/routes/resources.d.ts +25 -0
  389. package/dist/server/routes/resources.js +225 -0
  390. package/dist/server/routes/shared.d.ts +35 -0
  391. package/dist/server/routes/shared.js +305 -0
  392. package/dist/server/routes/shared.test.d.ts +1 -0
  393. package/dist/server/routes/shared.test.js +32 -0
  394. package/dist/server/{routes.sync.d.ts → routes/sync.d.ts} +5 -5
  395. package/dist/server/routes/sync.integration.test.d.ts +1 -0
  396. package/dist/server/routes/sync.integration.test.js +2003 -0
  397. package/dist/server/routes/sync.js +3143 -0
  398. package/dist/server/routes/tasks.d.ts +72 -0
  399. package/dist/server/routes/tasks.js +1252 -0
  400. package/dist/server/routes/workspaces.d.ts +32 -0
  401. package/dist/server/routes/workspaces.js +307 -0
  402. package/dist/server/routes.d.ts +8 -9
  403. package/dist/server/routes.js +968 -4092
  404. package/dist/server/routes.test.js +9043 -2415
  405. package/dist/server/runtimeMode.d.ts +1 -1
  406. package/dist/server/runtimeMode.js +3 -18
  407. package/dist/server/runtimeMode.test.js +3 -3
  408. package/dist/server/securityHeaders.d.ts +1 -1
  409. package/dist/server/securityHeaders.js +65 -6
  410. package/dist/server/securityHeaders.test.js +40 -2
  411. package/dist/services/templateEngine.test.d.ts +1 -0
  412. package/dist/{__tests__ → services}/templateEngine.test.js +1 -1
  413. package/dist/services/templates.test.d.ts +1 -0
  414. package/dist/{__tests__ → services}/templates.test.js +1 -1
  415. package/dist/services/workflowDocumentFixtures.test.d.ts +1 -0
  416. package/dist/{__tests__ → services}/workflowDocumentFixtures.test.js +4 -4
  417. package/dist/services/workflowExportMatrix.test.d.ts +1 -0
  418. package/dist/{__tests__ → services}/workflowExportMatrix.test.js +1 -1
  419. package/dist/services/workflowExportSnapshots.test.d.ts +1 -0
  420. package/dist/services/workflowGeneration.test.d.ts +1 -0
  421. package/dist/services/workflowSimulation.test.d.ts +1 -0
  422. package/dist/services/workflowSimulation.test.js +113 -0
  423. package/dist/services/workflowV2Policy.test.d.ts +1 -0
  424. package/dist/{__tests__ → services}/workflowV2Policy.test.js +2 -2
  425. package/dist/shared/aiProfileSurfaceType.d.ts +9 -0
  426. package/dist/shared/aiProfileSurfaceType.js +49 -0
  427. package/dist/shared/customTaxonomyLibrary.d.ts +11 -0
  428. package/dist/shared/customTaxonomyLibrary.js +276 -0
  429. package/dist/shared/customTaxonomyLibrary.test.d.ts +1 -0
  430. package/dist/shared/customTaxonomyLibrary.test.js +20 -0
  431. package/dist/shared/taxonomyCatalog.d.ts +19 -0
  432. package/dist/shared/taxonomyCatalog.js +36 -0
  433. package/dist/shared/taxonomyDefaults.d.ts +29 -0
  434. package/dist/shared/taxonomyDefaults.js +26 -0
  435. package/dist/shared/taxonomyLibrary.d.ts +15 -0
  436. package/dist/shared/taxonomyLibrary.js +98 -0
  437. package/dist/shared/taxonomyLibrary.test.d.ts +1 -0
  438. package/dist/shared/taxonomyLibrary.test.js +26 -0
  439. package/dist/shared/taxonomyState.d.ts +36 -0
  440. package/dist/shared/taxonomyState.js +73 -0
  441. package/dist/shared/taxonomyState.test.d.ts +1 -0
  442. package/dist/shared/taxonomyState.test.js +71 -0
  443. package/dist/shared/workflowDefaults.d.ts +9 -0
  444. package/dist/shared/workflowDefaults.js +11 -0
  445. package/dist/storage/documentIntegrity.d.ts +2 -0
  446. package/dist/storage/documentIntegrity.js +34 -17
  447. package/dist/storage/documentPurge.d.ts +2 -1
  448. package/dist/storage/documentPurge.js +35 -8
  449. package/dist/storage/index.d.ts +4 -0
  450. package/dist/storage/index.js +2 -0
  451. package/dist/storage/objectStorage.d.ts +1 -1
  452. package/dist/storage/objectStorageClient.d.ts +6 -4
  453. package/dist/storage/objectStorageClient.js +77 -29
  454. package/dist/storage/objectStorageClient.test.d.ts +1 -0
  455. package/dist/storage/objectStorageClient.test.js +57 -0
  456. package/dist/storage/objectStorageConfig.js +3 -1
  457. package/dist/storage/objectStorageConfig.test.d.ts +1 -0
  458. package/dist/storage/objectStorageConfig.test.js +38 -0
  459. package/dist/storage/postgresAdapter.d.ts +5 -0
  460. package/dist/storage/postgresAdapter.js +2 -2
  461. package/dist/storage/postgresAdapter.test.d.ts +1 -0
  462. package/dist/storage/postgresAdapter.test.js +29 -0
  463. package/dist/storage/taskAssetStore.d.ts +7 -4
  464. package/dist/storage/taskAssetStore.js +65 -60
  465. package/dist/storage/workspaceAssetStore.d.ts +122 -0
  466. package/dist/storage/workspaceAssetStore.js +496 -0
  467. package/dist/storage/workspaceAssetStore.test.d.ts +1 -0
  468. package/dist/storage/workspaceAssetStore.test.js +501 -0
  469. package/dist/sync/cloudSyncApi.d.ts +33 -2
  470. package/dist/sync/cloudSyncApi.js +37 -15
  471. package/dist/sync/cloudSyncApi.test.js +60 -3
  472. package/dist/sync/index.d.ts +2 -0
  473. package/dist/sync/index.js +3 -0
  474. package/dist/sync/syncService.d.ts +25 -0
  475. package/dist/sync/syncService.js +184 -16
  476. package/dist/sync/syncService.test.js +289 -25
  477. package/dist/sync/workspaceRepair.d.ts +114 -0
  478. package/dist/sync/workspaceRepair.js +566 -0
  479. package/dist/sync/workspaceSyncModel.d.ts +230 -2
  480. package/dist/sync/workspaceSyncModel.js +456 -17
  481. package/dist/sync/workspaceSyncModel.test.js +642 -0
  482. package/dist/sync/workspaceSyncState.d.ts +12 -0
  483. package/dist/sync/workspaceSyncState.js +12 -0
  484. package/dist/sync/workspaceSyncSurface.d.ts +33 -0
  485. package/dist/sync/workspaceSyncSurface.js +74 -0
  486. package/dist/types.d.ts +143 -18
  487. package/dist/types.js +9 -24
  488. package/dist/ui/assets/AgentsModule-Bdq3T1Ts.js +1 -0
  489. package/dist/ui/assets/AnnotatedAttachmentWorkspace-BaS2VwIr.css +1 -0
  490. package/dist/ui/assets/AnnotatedAttachmentWorkspace-jKUVo-PJ.js +3 -0
  491. package/dist/ui/assets/DocumentWorkspace-C_8T8oz-.css +1 -0
  492. package/dist/ui/assets/DocumentWorkspace-DuQa-uIC.js +250 -0
  493. package/dist/ui/assets/InitiativesModule-C8Pi2VTk.js +1 -0
  494. package/dist/ui/assets/PlansPage-B6BDECSi.js +1 -0
  495. package/dist/ui/assets/PlansPage-BlVC_lRq.css +1 -0
  496. package/dist/ui/assets/TaskSettings-BwB9NtmJ.css +1 -0
  497. package/dist/ui/assets/TaskSettings-DUJgRTPU.js +8 -0
  498. package/dist/ui/assets/WorkflowsModule-b6TLuHwJ.js +5 -0
  499. package/dist/ui/assets/index-BiiF1cLQ.css +1 -0
  500. package/dist/ui/assets/index-xBWLPOEb.js +6 -0
  501. package/dist/ui/assets/vendor-dnd-DRzYolkg.js +5 -0
  502. package/dist/ui/assets/{vendor-icons-ZFG3SGwo.js → vendor-icons-QZyhEwgT.js} +1 -1
  503. package/dist/ui/assets/{vendor-markdown-CKYkQHfj.js → vendor-markdown-BUxTU7dS.js} +1 -1
  504. package/dist/ui/assets/vendor-react-CKJs5o3c.js +61 -0
  505. package/dist/ui/assets/vendor-router-BbWMxlnO.js +3 -0
  506. package/dist/ui/branding/logos/logo.png +0 -0
  507. package/dist/ui/branding/logos/logo_dark.svg +27 -0
  508. package/dist/ui/branding/logos/logo_favicon.svg +35 -0
  509. package/dist/ui/branding/logos/logo_light.svg +27 -0
  510. package/dist/ui/favicon/apple-touch-icon.png +0 -0
  511. package/dist/ui/favicon/archive/phoenix/apple-touch-icon.png +0 -0
  512. package/dist/ui/favicon/archive/phoenix/favicon.ico +0 -0
  513. package/dist/ui/favicon/archive/phoenix/site.webmanifest +1 -0
  514. package/dist/ui/favicon/favicon-96x96.png +0 -0
  515. package/dist/ui/favicon/favicon.ico +0 -0
  516. package/dist/ui/favicon/favicon.svg +17 -0
  517. package/dist/ui/favicon/site.webmanifest +21 -1
  518. package/dist/ui/favicon/web-app-manifest-192x192.png +0 -0
  519. package/dist/ui/favicon/web-app-manifest-512x512.png +0 -0
  520. package/dist/ui/index.html +10 -7
  521. package/dist/ui/product/screen_01.png +0 -0
  522. package/dist/ui/product/screen_02.png +0 -0
  523. package/dist/ui/product/screen_03.png +0 -0
  524. package/dist/utils/annotatedAttachments.d.ts +20 -0
  525. package/dist/utils/annotatedAttachments.js +74 -0
  526. package/dist/utils/annotatedAttachments.test.d.ts +1 -0
  527. package/dist/utils/annotatedAttachments.test.js +108 -0
  528. package/dist/utils/assignees.d.ts +26 -0
  529. package/dist/utils/assignees.js +132 -0
  530. package/dist/utils/canonicalAssetKind.d.ts +16 -0
  531. package/dist/utils/canonicalAssetKind.js +39 -0
  532. package/dist/utils/canonicalAssetKind.test.d.ts +1 -0
  533. package/dist/utils/canonicalAssetKind.test.js +24 -0
  534. package/dist/utils/coalescedRefresh.d.ts +5 -0
  535. package/dist/utils/coalescedRefresh.js +16 -0
  536. package/dist/utils/coalescedRefresh.test.d.ts +1 -0
  537. package/dist/utils/coalescedRefresh.test.js +30 -0
  538. package/dist/utils/constants.d.ts +16 -3
  539. package/dist/utils/constants.icons.test.js +2 -1
  540. package/dist/utils/constants.js +70 -13
  541. package/dist/utils/contextAssetEvents.d.ts +7 -0
  542. package/dist/utils/contextAssetEvents.js +6 -0
  543. package/dist/utils/contextFiles.d.ts +14 -3
  544. package/dist/utils/contextFiles.js +40 -8
  545. package/dist/utils/documentManagerApi.d.ts +6 -0
  546. package/dist/utils/documentManagerApi.js +19 -0
  547. package/dist/utils/documentNames.d.ts +24 -0
  548. package/dist/utils/documentNames.js +91 -0
  549. package/dist/utils/documentReferences.d.ts +9 -0
  550. package/dist/utils/documentReferences.js +11 -0
  551. package/dist/utils/entityReferences.d.ts +9 -0
  552. package/dist/utils/entityReferences.js +32 -0
  553. package/dist/utils/imageReferences.d.ts +9 -0
  554. package/dist/utils/imageReferences.js +11 -0
  555. package/dist/utils/planParser.d.ts +2 -2
  556. package/dist/utils/planParser.js +11 -11
  557. package/dist/utils/planParser.test.d.ts +1 -0
  558. package/dist/{__tests__ → utils}/planParser.test.js +22 -22
  559. package/dist/utils/planningReferences.d.ts +17 -0
  560. package/dist/utils/planningReferences.js +30 -0
  561. package/dist/utils/planningReferences.test.d.ts +1 -0
  562. package/dist/utils/planningReferences.test.js +17 -0
  563. package/dist/utils/taskActivity.d.ts +9 -0
  564. package/dist/utils/taskActivity.js +147 -0
  565. package/dist/utils/taskActivity.test.d.ts +1 -0
  566. package/dist/utils/taskActivity.test.js +34 -0
  567. package/dist/utils/taskDraftDefaults.d.ts +21 -0
  568. package/dist/utils/taskDraftDefaults.js +40 -0
  569. package/dist/utils/taskDraftDefaults.test.d.ts +1 -0
  570. package/dist/utils/taskDraftDefaults.test.js +34 -0
  571. package/dist/utils/taskNormalization.d.ts +25 -0
  572. package/dist/utils/taskNormalization.js +161 -0
  573. package/dist/utils/taskNormalization.test.d.ts +1 -0
  574. package/dist/utils/taskNormalization.test.js +158 -0
  575. package/dist/utils/taskReferences.d.ts +5 -0
  576. package/dist/utils/taskReferences.js +11 -0
  577. package/dist/utils/taskforceApiClient.d.ts +2 -0
  578. package/dist/utils/taskforceApiClient.js +101 -3
  579. package/dist/utils/taskforceApiClient.test.d.ts +1 -0
  580. package/dist/utils/taskforceApiClient.test.js +44 -0
  581. package/dist/utils/taxonomyFilterOptions.d.ts +2 -0
  582. package/dist/utils/taxonomyFilterOptions.js +17 -0
  583. package/dist/utils/taxonomyFormDefaults.d.ts +3 -0
  584. package/dist/utils/taxonomyFormDefaults.js +28 -0
  585. package/dist/utils/taxonomyFormDefaults.test.d.ts +1 -0
  586. package/dist/utils/taxonomyFormDefaults.test.js +63 -0
  587. package/dist/utils/taxonomyLifecycle.d.ts +3 -0
  588. package/dist/utils/taxonomyLifecycle.js +17 -0
  589. package/dist/utils/taxonomySorting.d.ts +7 -0
  590. package/dist/utils/taxonomySorting.js +74 -0
  591. package/dist/utils/theme.d.ts +15 -0
  592. package/dist/utils/theme.js +23 -0
  593. package/dist/utils/uiNotice.d.ts +7 -0
  594. package/dist/utils/uiNotice.js +6 -0
  595. package/dist/utils/uiNotice.test.d.ts +1 -0
  596. package/dist/utils/uiNotice.test.js +18 -0
  597. package/dist/utils/workspaceIdentity.d.ts +14 -0
  598. package/dist/utils/workspaceIdentity.js +21 -0
  599. package/dist/utils/workspaceIdentity.test.js +30 -1
  600. package/dist/utils/workspaceSyncPresentation.d.ts +20 -0
  601. package/dist/utils/workspaceSyncPresentation.js +21 -0
  602. package/dist/utils/workspaceSyncPresentation.test.d.ts +1 -0
  603. package/dist/utils/workspaceSyncPresentation.test.js +36 -0
  604. package/package.json +30 -23
  605. package/scripts/bootstrap-playwright-auth.mjs +155 -0
  606. package/scripts/check-compat-boundaries.mjs +41 -0
  607. package/scripts/check-env.ts +9 -2
  608. package/scripts/debug-login.mjs +32 -0
  609. package/scripts/deploy-prod.sh +41 -0
  610. package/scripts/deploy-staging.sh +23 -0
  611. package/scripts/migrate-planning-model.ts +233 -0
  612. package/scripts/migrate-task-data-assets.mjs +408 -0
  613. package/scripts/playwright-auth.sh +11 -0
  614. package/scripts/run-dev-proxy.sh +2 -0
  615. package/scripts/update-homebrew-formula.sh +2 -2
  616. package/scripts/with-esbuild.sh +11 -0
  617. package/src/resources/templates/WORKFLOW_V2_POLICY.md +1 -1
  618. package/src/resources/templates/workflow-sources/workflowDocs.mjs +62 -52
  619. package/src/resources/templates/workflows/collaborate.yaml +11 -5
  620. package/src/resources/templates/workflows/do.yaml +8 -2
  621. package/src/resources/templates/workflows/evaluate.yaml +13 -7
  622. package/src/resources/templates/workflows/execute.yaml +40 -23
  623. package/src/resources/templates/workflows/plan.yaml +15 -8
  624. package/src/resources/templates/workflows/review.yaml +14 -8
  625. package/dist/TaskforceScreenshot.d.ts +0 -11
  626. package/dist/TaskforceScreenshot.js +0 -113
  627. package/dist/compat/priorityCompat.d.ts +0 -2
  628. package/dist/compat/priorityCompat.js +0 -24
  629. package/dist/components/features/settings/TaxonomyManager 2.d.ts +0 -52
  630. package/dist/components/features/settings/TaxonomyManager 2.js +0 -75
  631. package/dist/components/task/TaskKanban 2.d.ts +0 -76
  632. package/dist/components/task/TaskKanban 2.js +0 -1004
  633. package/dist/components/task/TaskScreenshotSection.d.ts +0 -15
  634. package/dist/components/task/TaskScreenshotSection.js +0 -22
  635. package/dist/components/views/StandaloneLayout 2.d.ts +0 -8
  636. package/dist/components/views/StandaloneLayout 2.js +0 -1660
  637. package/dist/core/InitiativeTemplates.test.js +0 -50
  638. package/dist/core/ParentLifecycle.test.js +0 -98
  639. package/dist/migrations/clientMigrations.d.ts +0 -9
  640. package/dist/migrations/clientMigrations.js +0 -74
  641. package/dist/migrations/flags.d.ts +0 -6
  642. package/dist/migrations/flags.js +0 -8
  643. package/dist/migrations/taskMigrations.d.ts +0 -71
  644. package/dist/migrations/taskMigrations.js +0 -236
  645. package/dist/migrations/taskMigrations.test.js +0 -34
  646. package/dist/server/routes.billing.js +0 -991
  647. package/dist/server/routes.billing.test.js +0 -387
  648. package/dist/server/routes.sync.integration.test.js +0 -470
  649. package/dist/server/routes.sync.js +0 -1141
  650. package/dist/soak/syncSoakHarness.d.ts +0 -6
  651. package/dist/soak/syncSoakHarness.js +0 -10
  652. package/dist/ui/assets/index-0W5vyCpn.css +0 -1
  653. package/dist/ui/assets/index-MCZuNNfn.js +0 -23
  654. package/dist/ui/assets/logo_white_01-0Vx-iIIv.png +0 -0
  655. package/dist/ui/assets/vendor-dnd-BI1K77C1.js +0 -5
  656. package/dist/ui/assets/vendor-react-DdIDsh7f.js +0 -9
  657. package/dist/ui/assets/vendor-router-BtiNU7Dl.js +0 -3
  658. package/dist/utils/taskHierarchy.d.ts +0 -4
  659. package/dist/utils/taskHierarchy.js +0 -69
  660. package/dist/utils/taskHierarchy.test.js +0 -38
  661. package/dist/utils/taskParenting.d.ts +0 -3
  662. package/dist/utils/taskParenting.js +0 -24
  663. package/dist/utils/taskParenting.test.js +0 -21
  664. package/dist/utils/taskRelations.d.ts +0 -6
  665. package/dist/utils/taskRelations.js +0 -24
  666. package/dist/utils/taskRelations.test.js +0 -34
  667. package/scripts/check-compat-boundaries.js +0 -48
  668. /package/dist/{__tests__/planParser.test.d.ts → TaskforceCore.filter-persistence.test.d.ts} +0 -0
  669. /package/dist/{__tests__/releaseVersion.test.d.ts → TaskforceCore.routing.test.d.ts} +0 -0
  670. /package/dist/{__tests__/structuredCommentValidation.test.d.ts → annotatedAttachments/AnnotatedAttachmentSessionStore.test.d.ts} +0 -0
  671. /package/dist/{__tests__/structuredDocValidation.test.d.ts → annotatedAttachments/payload.test.d.ts} +0 -0
  672. /package/dist/{__tests__/templateEngine.test.d.ts → annotatedAttachments/types.js} +0 -0
  673. /package/dist/{__tests__/templates.test.d.ts → compat/workspaceSyncCompat.test.d.ts} +0 -0
  674. /package/dist/{__tests__/workflowDocumentFixtures.test.d.ts → components/features/AgentsModule.test.d.ts} +0 -0
  675. /package/dist/{__tests__/workflowExportMatrix.test.d.ts → components/features/AnnotatedAttachmentWorkspace.test.d.ts} +0 -0
  676. /package/dist/{__tests__/workflowExportSnapshots.test.d.ts → components/features/DocumentWorkspace.test.d.ts} +0 -0
  677. /package/dist/{__tests__/workflowGeneration.test.d.ts → components/features/settings/TaxonomyLibraryManager.test.d.ts} +0 -0
  678. /package/dist/{__tests__/workflowV2Policy.test.d.ts → components/task/TaskActionHeader.test.d.ts} +0 -0
  679. /package/dist/{core/InitiativeTemplates.test.d.ts → components/task/TaskKanban.planning-drop.test.d.ts} +0 -0
  680. /package/dist/{core/ParentLifecycle.test.d.ts → components/task/TaskStatusActions.test.d.ts} +0 -0
  681. /package/dist/{migrations/taskMigrations.test.d.ts → components/ui/TopNoticeLayer.test.d.ts} +0 -0
  682. /package/dist/{server/routes.billing.test.d.ts → components/views/AppShellHeader.test.d.ts} +0 -0
  683. /package/dist/{server/routes.sync.integration.test.d.ts → components/views/PlanComparisonPage.test.d.ts} +0 -0
  684. /package/dist/{utils/taskHierarchy.test.d.ts → components/views/chrome/WorkspaceChromeSections.test.d.ts} +0 -0
  685. /package/dist/{utils/taskParenting.test.d.ts → components/views/chrome/workspaceChrome.test.d.ts} +0 -0
  686. /package/dist/{utils/taskRelations.test.d.ts → components/views/panels/DocumentFilterToolbar.test.d.ts} +0 -0
  687. /package/dist/public/{android-chrome-192x192.png → archive/phoenix/android-chrome-192x192.png} +0 -0
  688. /package/dist/public/{android-chrome-512x512.png → archive/phoenix/android-chrome-512x512.png} +0 -0
  689. /package/dist/public/{favicon-16x16.png → archive/phoenix/favicon-16x16.png} +0 -0
  690. /package/dist/public/{favicon-32x32.png → archive/phoenix/favicon-32x32.png} +0 -0
  691. /package/dist/public/{favicon.png → archive/phoenix/favicon.png} +0 -0
  692. /package/dist/{__tests__ → services}/workflowExportSnapshots.test.js +0 -0
  693. /package/dist/{__tests__ → services}/workflowGeneration.test.js +0 -0
  694. /package/dist/ui/favicon/{android-chrome-192x192.png → archive/phoenix/android-chrome-192x192.png} +0 -0
  695. /package/dist/ui/favicon/{android-chrome-512x512.png → archive/phoenix/android-chrome-512x512.png} +0 -0
  696. /package/dist/ui/favicon/{favicon-16x16.png → archive/phoenix/favicon-16x16.png} +0 -0
  697. /package/dist/ui/favicon/{favicon-32x32.png → archive/phoenix/favicon-32x32.png} +0 -0
  698. /package/dist/ui/favicon/{favicon.png → archive/phoenix/favicon.png} +0 -0
@@ -0,0 +1,1631 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import { Readable } from 'stream';
3
+ import Database from 'better-sqlite3';
4
+ import { registerBillingRoutes } from './billing';
5
+ const mockConstructEvent = vi.fn();
6
+ const mockPricesList = vi.fn(async () => ({ data: [], has_more: false }));
7
+ const mockPriceRetrieve = vi.fn(async (id) => ({ id, active: true, currency: 'usd', unit_amount: 1500, recurring: { interval: 'month' } }));
8
+ const mockCheckoutCreate = vi.fn(async () => ({ id: 'cs_test', url: 'https://example.test/checkout' }));
9
+ const mockCheckoutRetrieve = vi.fn(async (id) => ({ id, mode: 'subscription', customer: 'cus_123', subscription: 'sub_123', metadata: { userId: 'u1' } }));
10
+ const mockPortalCreate = vi.fn(async () => ({ id: 'bps_test', url: 'https://example.test/portal' }));
11
+ const mockSubscriptionCancel = vi.fn(async () => ({ id: 'sub_test', status: 'canceled' }));
12
+ const mockSubscriptionList = vi.fn(async () => ({ data: [] }));
13
+ const mockCustomersList = vi.fn(async () => ({ data: [] }));
14
+ const mockSubscriptionUpdate = vi.fn(async (id, input) => ({
15
+ id,
16
+ customer: 'cus_123',
17
+ status: 'active',
18
+ trial_end: null,
19
+ metadata: {
20
+ userId: 'u1',
21
+ planVersionId: String(input?.metadata?.planVersionId || 'pro-v1')
22
+ },
23
+ items: {
24
+ data: [{
25
+ id: 'si_123',
26
+ current_period_end: Math.floor(Date.now() / 1000) + 86400,
27
+ price: { id: String(input?.items?.[0]?.price || 'price_pro_month'), recurring: { interval: 'month' } }
28
+ }]
29
+ }
30
+ }));
31
+ const mockSubscriptionRetrieve = vi.fn(async (id) => ({
32
+ id,
33
+ customer: 'cus_123',
34
+ status: 'active',
35
+ trial_end: null,
36
+ metadata: { userId: 'u1', planVersionId: 'pro-v1' },
37
+ items: {
38
+ data: [{
39
+ id: 'si_123',
40
+ current_period_end: Math.floor(Date.now() / 1000) + 86400,
41
+ price: { id: 'price_pro_month', recurring: { interval: 'month' } }
42
+ }]
43
+ }
44
+ }));
45
+ vi.mock('stripe', () => ({
46
+ default: class StripeMock {
47
+ webhooks = {
48
+ constructEvent: mockConstructEvent
49
+ };
50
+ prices = {
51
+ list: mockPricesList,
52
+ retrieve: mockPriceRetrieve
53
+ };
54
+ checkout = {
55
+ sessions: {
56
+ create: mockCheckoutCreate,
57
+ retrieve: mockCheckoutRetrieve
58
+ }
59
+ };
60
+ billingPortal = {
61
+ sessions: {
62
+ create: mockPortalCreate
63
+ }
64
+ };
65
+ customers = {
66
+ list: mockCustomersList
67
+ };
68
+ subscriptions = {
69
+ cancel: mockSubscriptionCancel,
70
+ retrieve: mockSubscriptionRetrieve,
71
+ update: mockSubscriptionUpdate,
72
+ list: mockSubscriptionList
73
+ };
74
+ constructor(_apiKey) { }
75
+ }
76
+ }));
77
+ function createMockReq(body, headers = {}) {
78
+ const req = Readable.from([Buffer.from(body, 'utf8')]);
79
+ req.headers = headers;
80
+ return req;
81
+ }
82
+ function createMockRes() {
83
+ const response = {
84
+ statusCode: 200,
85
+ headers: {},
86
+ body: '',
87
+ writeHead: vi.fn((statusCode, headers) => {
88
+ response.statusCode = statusCode;
89
+ response.headers = headers || {};
90
+ return response;
91
+ }),
92
+ end: vi.fn((payload) => {
93
+ response.body = String(payload || '');
94
+ return response;
95
+ })
96
+ };
97
+ return response;
98
+ }
99
+ function setupBillingSchema(db) {
100
+ db.exec(`
101
+ CREATE TABLE user_billing (
102
+ user_id TEXT PRIMARY KEY,
103
+ tenant_id TEXT NOT NULL,
104
+ stripe_customer_id TEXT,
105
+ stripe_subscription_id TEXT,
106
+ stripe_price_id TEXT,
107
+ stripe_status TEXT,
108
+ tier TEXT NOT NULL DEFAULT '',
109
+ billing_interval TEXT,
110
+ entitlement_state TEXT NOT NULL DEFAULT 'active',
111
+ trial_end TEXT,
112
+ current_period_end TEXT,
113
+ effective_until TEXT,
114
+ created_at TEXT NOT NULL,
115
+ updated_at TEXT NOT NULL
116
+ );
117
+ CREATE TABLE stripe_webhook_events (
118
+ event_id TEXT PRIMARY KEY,
119
+ tenant_id TEXT NOT NULL,
120
+ event_type TEXT NOT NULL,
121
+ processed_at TEXT NOT NULL
122
+ );
123
+ CREATE TABLE stripe_webhook_failures (
124
+ id TEXT PRIMARY KEY,
125
+ tenant_id TEXT NOT NULL,
126
+ event_id TEXT NOT NULL,
127
+ event_type TEXT,
128
+ reason TEXT NOT NULL,
129
+ payload_excerpt TEXT,
130
+ purge_after TEXT NOT NULL,
131
+ created_at TEXT NOT NULL
132
+ );
133
+ CREATE TABLE billing_price_mappings (
134
+ tenant_id TEXT NOT NULL,
135
+ plan_version_id TEXT NOT NULL,
136
+ billing_interval TEXT NOT NULL,
137
+ stripe_price_id TEXT NOT NULL,
138
+ pricing_type TEXT NOT NULL DEFAULT 'stripe',
139
+ active INTEGER NOT NULL DEFAULT 1,
140
+ unit_amount INTEGER,
141
+ currency TEXT,
142
+ created_at TEXT NOT NULL,
143
+ updated_at TEXT NOT NULL,
144
+ PRIMARY KEY (tenant_id, plan_version_id, billing_interval, stripe_price_id)
145
+ );
146
+ CREATE UNIQUE INDEX idx_billing_price_mappings_active_interval
147
+ ON billing_price_mappings (tenant_id, plan_version_id, billing_interval)
148
+ WHERE active = 1;
149
+ CREATE TABLE plan_versions (
150
+ plan_version_id TEXT PRIMARY KEY,
151
+ tenant_id TEXT NOT NULL,
152
+ plan_id TEXT NOT NULL,
153
+ version_number INTEGER NOT NULL,
154
+ is_default INTEGER NOT NULL DEFAULT 0,
155
+ seat_limit INTEGER,
156
+ metadata_json TEXT NOT NULL DEFAULT '{}',
157
+ created_at TEXT NOT NULL,
158
+ updated_at TEXT NOT NULL
159
+ );
160
+ CREATE TABLE plan_catalog (
161
+ plan_id TEXT PRIMARY KEY,
162
+ tenant_id TEXT NOT NULL,
163
+ display_name TEXT NOT NULL,
164
+ status TEXT NOT NULL DEFAULT 'active',
165
+ created_at TEXT NOT NULL,
166
+ updated_at TEXT NOT NULL
167
+ );
168
+ CREATE TABLE account_entitlements (
169
+ account_id TEXT PRIMARY KEY,
170
+ tenant_id TEXT NOT NULL,
171
+ plan_version_id TEXT NOT NULL,
172
+ entitlement_state TEXT NOT NULL DEFAULT 'active',
173
+ effective_at TEXT,
174
+ effective_until TEXT,
175
+ updated_by TEXT,
176
+ created_at TEXT NOT NULL,
177
+ updated_at TEXT NOT NULL
178
+ );
179
+ CREATE TABLE system_settings (
180
+ tenant_id TEXT NOT NULL,
181
+ settings_key TEXT NOT NULL,
182
+ payload TEXT,
183
+ updated_at TEXT NOT NULL,
184
+ PRIMARY KEY (tenant_id, settings_key)
185
+ );
186
+ `);
187
+ }
188
+ function buildBillingRouteHandlers(db, opts) {
189
+ const routes = new Map();
190
+ const tenantId = 'default';
191
+ const core = {
192
+ getDatabaseAdapter: () => db,
193
+ getTenantId: () => tenantId,
194
+ createUserBillingRecord: (userId) => {
195
+ const now = new Date().toISOString();
196
+ db.prepare(`
197
+ INSERT INTO user_billing (
198
+ user_id, tenant_id, tier, entitlement_state, created_at, updated_at
199
+ ) VALUES (?, ?, '', 'active', ?, ?)
200
+ ON CONFLICT (user_id) DO NOTHING
201
+ `).run(userId, tenantId, now, now);
202
+ },
203
+ listPlanCatalog: () => [
204
+ { planId: 'pro', displayName: 'Pro Plan', status: 'active', createdAt: '', updatedAt: '' },
205
+ { planId: 'inactive-plan', displayName: 'Inactive', status: 'archived', createdAt: '', updatedAt: '' },
206
+ { planId: 'empty-plan', displayName: 'Empty', status: 'active', createdAt: '', updatedAt: '' },
207
+ { planId: 'starter', displayName: 'Starter', status: 'active', createdAt: '', updatedAt: '' }
208
+ ],
209
+ listPlanVersions: (planId) => {
210
+ return db.prepare(`
211
+ SELECT plan_version_id, plan_id, version_number, is_default, seat_limit, metadata_json
212
+ FROM plan_versions
213
+ WHERE tenant_id = ? AND plan_id = ?
214
+ ORDER BY version_number ASC
215
+ `).all(tenantId, planId).map((row) => {
216
+ const metadata = row?.metadata_json ? JSON.parse(String(row.metadata_json)) : {};
217
+ return {
218
+ planVersionId: String(row?.plan_version_id || ''),
219
+ planId: String(row?.plan_id || ''),
220
+ versionNumber: Number(row?.version_number || 1),
221
+ isDefault: Number(row?.is_default || 0) === 1,
222
+ seatLimit: row?.seat_limit === null || row?.seat_limit === undefined ? null : Number(row.seat_limit),
223
+ trialDays: Number.isFinite(Number(metadata?.trialDays)) ? Number(metadata.trialDays) : 0,
224
+ metadata,
225
+ features: {},
226
+ createdAt: '',
227
+ updatedAt: ''
228
+ };
229
+ });
230
+ },
231
+ getAccountEntitlement: () => ({
232
+ accountId: 'u1',
233
+ planVersionId: 'pro-v1',
234
+ planId: 'pro',
235
+ state: 'active',
236
+ effectiveAt: null,
237
+ effectiveUntil: null,
238
+ updatedBy: null,
239
+ createdAt: '',
240
+ updatedAt: ''
241
+ }),
242
+ getAccountAccessStatus: () => ({
243
+ gate: 'ok',
244
+ hasEntitlement: true,
245
+ canAccessApp: true,
246
+ canAccessPlans: true,
247
+ signupMonetizationStrategy: 'auto_assign_default',
248
+ planSelectionRequired: false,
249
+ message: null,
250
+ entitlement: {
251
+ accountId: 'u1',
252
+ planVersionId: 'pro-v1',
253
+ planId: 'pro',
254
+ state: 'active',
255
+ effectiveAt: null,
256
+ effectiveUntil: null
257
+ }
258
+ }),
259
+ getBillingMirrorProfileForPlanVersion: (planVersionId) => ({
260
+ planId: planVersionId.startsWith('pro') ? 'pro' : 'starter',
261
+ entitlementState: 'active'
262
+ }),
263
+ resolveUserIdentity: () => opts?.userIdentity === undefined
264
+ ? {
265
+ userId: 'u1',
266
+ email: 'owner@example.com',
267
+ systemRole: 'user',
268
+ betaAccess: true
269
+ }
270
+ : opts.userIdentity,
271
+ updateAccountEntitlement: vi.fn().mockReturnValue({
272
+ accountId: 'u1',
273
+ planVersionId: 'starter-v1',
274
+ planId: 'starter',
275
+ state: 'active',
276
+ effectiveAt: null,
277
+ effectiveUntil: null
278
+ })
279
+ };
280
+ registerBillingRoutes({
281
+ addRoute: (method, path, handler) => routes.set(`${method} ${path}`, handler),
282
+ core,
283
+ context: {
284
+ getRuntimeMode: () => 'cloud'
285
+ },
286
+ ensureAuthenticatedUser: () => true,
287
+ ensureAdminRole: () => opts?.isAdmin !== false,
288
+ requestUserId: () => 'u1',
289
+ resolveErrorStatusCode: (_error, fallback = 500) => fallback,
290
+ auditAdminAction: () => { }
291
+ });
292
+ return routes;
293
+ }
294
+ describe('Billing Webhook Routes', () => {
295
+ const originalEnv = { ...process.env };
296
+ let db;
297
+ let webhookHandler;
298
+ let routeHandlers;
299
+ beforeEach(() => {
300
+ vi.clearAllMocks();
301
+ db = new Database(':memory:');
302
+ setupBillingSchema(db);
303
+ routeHandlers = buildBillingRouteHandlers(db);
304
+ webhookHandler = routeHandlers.get('POST /api/taskforce/billing/webhook');
305
+ process.env.TASKFORCE_BILLING_ENABLED = 'true';
306
+ process.env.TASKFORCE_STRIPE_SECRET_KEY = 'sk_test_123';
307
+ process.env.TASKFORCE_STRIPE_WEBHOOK_SECRET = 'whsec_123';
308
+ });
309
+ afterEach(() => {
310
+ process.env = { ...originalEnv };
311
+ db.close();
312
+ });
313
+ it('returns 400 for invalid webhook signature', async () => {
314
+ mockConstructEvent.mockImplementation(() => {
315
+ throw new Error('invalid signature');
316
+ });
317
+ const req = createMockReq('{"id":"evt_bad"}', {
318
+ 'x-taskforce-runtime-mode': 'cloud',
319
+ 'stripe-signature': 'bad_signature'
320
+ });
321
+ const res = createMockRes();
322
+ await webhookHandler(req, res, {});
323
+ expect(res.statusCode).toBe(400);
324
+ });
325
+ it('returns 200 no-op for duplicate event id', async () => {
326
+ const now = new Date().toISOString();
327
+ db.prepare(`
328
+ INSERT INTO stripe_webhook_events (event_id, tenant_id, event_type, processed_at)
329
+ VALUES (?, 'default', ?, ?)
330
+ `).run('evt_dupe', 'invoice.paid', now);
331
+ mockConstructEvent.mockReturnValue({
332
+ id: 'evt_dupe',
333
+ type: 'invoice.paid',
334
+ data: {
335
+ object: {
336
+ customer: 'cus_1',
337
+ lines: { data: [{ price: { id: 'price_1', recurring: { interval: 'month' } } }] }
338
+ }
339
+ }
340
+ });
341
+ const req = createMockReq('{"id":"evt_dupe"}', {
342
+ 'x-taskforce-runtime-mode': 'cloud',
343
+ 'stripe-signature': 'sig'
344
+ });
345
+ const res = createMockRes();
346
+ await webhookHandler(req, res, {});
347
+ expect(res.statusCode).toBe(200);
348
+ expect(JSON.parse(res.body)).toEqual({ received: true, duplicate: true });
349
+ });
350
+ it('returns 500 on processing failure so Stripe can retry', async () => {
351
+ mockConstructEvent.mockReturnValue({
352
+ id: 'evt_fail',
353
+ type: 'invoice.paid',
354
+ data: {
355
+ object: {
356
+ customer: 'cus_missing',
357
+ lines: { data: [{ price: { id: 'price_1', recurring: { interval: 'month' } } }] }
358
+ }
359
+ }
360
+ });
361
+ const req = createMockReq('{"id":"evt_fail"}', {
362
+ 'x-taskforce-runtime-mode': 'cloud',
363
+ 'stripe-signature': 'sig'
364
+ });
365
+ const res = createMockRes();
366
+ await webhookHandler(req, res, {});
367
+ expect(res.statusCode).toBe(500);
368
+ const failures = db.prepare(`SELECT event_id, reason FROM stripe_webhook_failures WHERE event_id = ?`).all('evt_fail');
369
+ expect(failures.length).toBeGreaterThan(0);
370
+ expect(failures[0]?.reason || '').toContain('Unable to resolve user for invoice event.');
371
+ });
372
+ it('preserves existing entitlement on unknown price_id and logs failure', async () => {
373
+ const now = new Date().toISOString();
374
+ db.prepare(`
375
+ INSERT INTO user_billing (user_id, tenant_id, stripe_customer_id, entitlement_state, created_at, updated_at)
376
+ VALUES ('u1', 'default', 'cus_123', 'active', ?, ?)
377
+ `).run(now, now);
378
+ db.prepare(`
379
+ INSERT INTO account_entitlements (
380
+ account_id, tenant_id, plan_version_id, entitlement_state, effective_at, created_at, updated_at
381
+ ) VALUES ('u1', 'default', 'starter-v1', 'active', ?, ?, ?)
382
+ `).run(now, now, now);
383
+ mockConstructEvent.mockReturnValue({
384
+ id: 'evt_unknown_price',
385
+ type: 'customer.subscription.updated',
386
+ data: {
387
+ object: {
388
+ id: 'sub_123',
389
+ customer: 'cus_123',
390
+ status: 'active',
391
+ trial_end: null,
392
+ metadata: {},
393
+ items: {
394
+ data: [{
395
+ current_period_end: Math.floor(Date.now() / 1000) + 86400,
396
+ price: { id: 'price_unknown', recurring: { interval: 'month' } }
397
+ }]
398
+ }
399
+ }
400
+ }
401
+ });
402
+ const req = createMockReq('{"id":"evt_unknown_price"}', {
403
+ 'x-taskforce-runtime-mode': 'cloud',
404
+ 'stripe-signature': 'sig'
405
+ });
406
+ const res = createMockRes();
407
+ await webhookHandler(req, res, {});
408
+ expect(res.statusCode).toBe(200);
409
+ const entitlement = db.prepare(`
410
+ SELECT plan_version_id
411
+ FROM account_entitlements
412
+ WHERE account_id = 'u1' AND tenant_id = 'default'
413
+ `).get();
414
+ expect(entitlement.plan_version_id).toBe('starter-v1');
415
+ const failure = db.prepare(`
416
+ SELECT reason
417
+ FROM stripe_webhook_failures
418
+ WHERE event_id = 'evt_unknown_price'
419
+ ORDER BY created_at DESC
420
+ LIMIT 1
421
+ `).get();
422
+ expect(failure.reason).toContain('Unknown price mapping');
423
+ });
424
+ it('purges expired webhook failures during webhook processing', async () => {
425
+ const past = new Date(Date.now() - 48 * 60 * 60 * 1000).toISOString();
426
+ const now = new Date().toISOString();
427
+ db.prepare(`
428
+ INSERT INTO stripe_webhook_failures (id, tenant_id, event_id, event_type, reason, payload_excerpt, purge_after, created_at)
429
+ VALUES ('whf_old', 'default', 'evt_old', 'invoice.paid', 'old', '{}', ?, ?)
430
+ `).run(past, now);
431
+ db.prepare(`
432
+ INSERT INTO stripe_webhook_events (event_id, tenant_id, event_type, processed_at)
433
+ VALUES ('evt_dup_for_purge', 'default', 'invoice.paid', ?)
434
+ `).run(now);
435
+ mockConstructEvent.mockReturnValue({
436
+ id: 'evt_dup_for_purge',
437
+ type: 'invoice.paid',
438
+ data: { object: { customer: 'cus_1', lines: { data: [] } } }
439
+ });
440
+ const req = createMockReq('{"id":"evt_dup_for_purge"}', {
441
+ 'x-taskforce-runtime-mode': 'cloud',
442
+ 'stripe-signature': 'sig'
443
+ });
444
+ const res = createMockRes();
445
+ await webhookHandler(req, res, {});
446
+ expect(res.statusCode).toBe(200);
447
+ const row = db.prepare(`SELECT id FROM stripe_webhook_failures WHERE id = 'whf_old'`).get();
448
+ expect(row).toBeUndefined();
449
+ });
450
+ it('creates checkout session with plans success/cancel URLs and selection context', async () => {
451
+ const now = new Date().toISOString();
452
+ db.prepare(`
453
+ INSERT INTO plan_versions (plan_version_id, tenant_id, plan_id, version_number, is_default, metadata_json, created_at, updated_at)
454
+ VALUES ('collab-v1', 'default', 'collab', 1, 1, '{}', ?, ?)
455
+ `).run(now, now);
456
+ db.prepare(`
457
+ INSERT INTO billing_price_mappings (tenant_id, plan_version_id, billing_interval, stripe_price_id, active, created_at, updated_at)
458
+ VALUES ('default', 'collab-v1', 'month', 'price_collab_month', 1, ?, ?)
459
+ `).run(now, now);
460
+ const checkoutHandler = routeHandlers.get('POST /api/taskforce/billing/checkout-session');
461
+ expect(checkoutHandler).toBeTypeOf('function');
462
+ const req = createMockReq(JSON.stringify({
463
+ planVersionId: 'collab-v1',
464
+ interval: 'month'
465
+ }), {
466
+ 'x-taskforce-runtime-mode': 'cloud',
467
+ host: 'app.taskforcehq.ai'
468
+ });
469
+ const res = createMockRes();
470
+ await checkoutHandler(req, res, {});
471
+ expect(res.statusCode).toBe(200);
472
+ expect(mockCheckoutCreate).toHaveBeenCalledTimes(1);
473
+ const firstCall = mockCheckoutCreate.mock.calls[0];
474
+ const args = (Array.isArray(firstCall) ? firstCall[0] : {});
475
+ expect(String(args.success_url || '')).toContain('/?screen=plans&checkout=success');
476
+ expect(String(args.cancel_url || '')).toContain('/?screen=plans&checkout=cancel');
477
+ expect(String(args.cancel_url || '')).toContain('planId=collab');
478
+ expect(String(args.cancel_url || '')).toContain('planVersionId=collab-v1');
479
+ expect(String(args.cancel_url || '')).toContain('interval=month');
480
+ expect(args.line_items?.[0]?.price).toBe('price_collab_month');
481
+ expect(args.customer).toBeUndefined();
482
+ expect(args.customer_email).toBe('owner@example.com');
483
+ });
484
+ it('uses per-version trial days when creating checkout', async () => {
485
+ const now = new Date().toISOString();
486
+ db.prepare(`
487
+ INSERT INTO plan_versions (plan_version_id, tenant_id, plan_id, version_number, is_default, metadata_json, created_at, updated_at)
488
+ VALUES ('collab-v1', 'default', 'collab', 1, 1, '{"trialDays":21}', ?, ?)
489
+ `).run(now, now);
490
+ db.prepare(`
491
+ INSERT INTO billing_price_mappings (tenant_id, plan_version_id, billing_interval, stripe_price_id, active, created_at, updated_at)
492
+ VALUES ('default', 'collab-v1', 'month', 'price_collab_month', 1, ?, ?)
493
+ `).run(now, now);
494
+ const checkoutHandler = routeHandlers.get('POST /api/taskforce/billing/checkout-session');
495
+ const req = createMockReq(JSON.stringify({
496
+ planVersionId: 'collab-v1',
497
+ interval: 'month'
498
+ }), {
499
+ 'x-taskforce-runtime-mode': 'cloud',
500
+ host: 'app.taskforcehq.ai'
501
+ });
502
+ const res = createMockRes();
503
+ await checkoutHandler(req, res, {});
504
+ expect(res.statusCode).toBe(200);
505
+ const args = mockCheckoutCreate.mock.calls.at(-1)?.[0];
506
+ expect(args.subscription_data?.trial_period_days).toBe(21);
507
+ });
508
+ it('omits Stripe trial period when the selected version trialDays is 0', async () => {
509
+ const now = new Date().toISOString();
510
+ db.prepare(`
511
+ INSERT INTO plan_versions (plan_version_id, tenant_id, plan_id, version_number, is_default, metadata_json, created_at, updated_at)
512
+ VALUES ('collab-v1', 'default', 'collab', 1, 1, '{"trialDays":0}', ?, ?)
513
+ `).run(now, now);
514
+ db.prepare(`
515
+ INSERT INTO billing_price_mappings (tenant_id, plan_version_id, billing_interval, stripe_price_id, active, created_at, updated_at)
516
+ VALUES ('default', 'collab-v1', 'month', 'price_collab_month', 1, ?, ?)
517
+ `).run(now, now);
518
+ const checkoutHandler = routeHandlers.get('POST /api/taskforce/billing/checkout-session');
519
+ const req = createMockReq(JSON.stringify({
520
+ planVersionId: 'collab-v1',
521
+ interval: 'month'
522
+ }), {
523
+ 'x-taskforce-runtime-mode': 'cloud',
524
+ host: 'app.taskforcehq.ai'
525
+ });
526
+ const res = createMockRes();
527
+ await checkoutHandler(req, res, {});
528
+ expect(res.statusCode).toBe(200);
529
+ const args = mockCheckoutCreate.mock.calls.at(-1)?.[0];
530
+ expect(args.subscription_data?.trial_period_days).toBeUndefined();
531
+ });
532
+ it('creates checkout session with existing stripe customer and no customer_email override', async () => {
533
+ const now = new Date().toISOString();
534
+ db.prepare(`
535
+ INSERT INTO plan_versions (plan_version_id, tenant_id, plan_id, version_number, is_default, metadata_json, created_at, updated_at)
536
+ VALUES ('collab-v1', 'default', 'collab', 1, 1, '{}', ?, ?)
537
+ `).run(now, now);
538
+ db.prepare(`
539
+ INSERT INTO billing_price_mappings (tenant_id, plan_version_id, billing_interval, stripe_price_id, active, created_at, updated_at)
540
+ VALUES ('default', 'collab-v1', 'month', 'price_collab_month', 1, ?, ?)
541
+ `).run(now, now);
542
+ db.prepare(`
543
+ INSERT INTO user_billing (user_id, tenant_id, stripe_customer_id, entitlement_state, created_at, updated_at)
544
+ VALUES ('u1', 'default', 'cus_existing', 'active', ?, ?)
545
+ `).run(now, now);
546
+ const checkoutHandler = routeHandlers.get('POST /api/taskforce/billing/checkout-session');
547
+ const req = createMockReq(JSON.stringify({
548
+ planVersionId: 'collab-v1',
549
+ interval: 'month'
550
+ }), {
551
+ 'x-taskforce-runtime-mode': 'cloud',
552
+ host: 'app.taskforcehq.ai'
553
+ });
554
+ const res = createMockRes();
555
+ await checkoutHandler(req, res, {});
556
+ expect(res.statusCode).toBe(200);
557
+ const args = mockCheckoutCreate.mock.calls.at(-1)?.[0];
558
+ expect(args.customer).toBe('cus_existing');
559
+ expect(args.customer_email).toBeUndefined();
560
+ });
561
+ it('reuses an existing Stripe customer discovered by account email before creating checkout', async () => {
562
+ const now = new Date().toISOString();
563
+ db.prepare(`
564
+ INSERT INTO plan_versions (plan_version_id, tenant_id, plan_id, version_number, is_default, metadata_json, created_at, updated_at)
565
+ VALUES ('collab-v1', 'default', 'collab', 1, 1, '{}', ?, ?)
566
+ `).run(now, now);
567
+ db.prepare(`
568
+ INSERT INTO billing_price_mappings (tenant_id, plan_version_id, billing_interval, stripe_price_id, active, created_at, updated_at)
569
+ VALUES ('default', 'collab-v1', 'month', 'price_collab_month', 1, ?, ?)
570
+ `).run(now, now);
571
+ mockCustomersList.mockResolvedValueOnce({
572
+ data: [{ id: 'cus_email_match' }]
573
+ });
574
+ mockSubscriptionList.mockResolvedValueOnce({ data: [] });
575
+ const checkoutHandler = routeHandlers.get('POST /api/taskforce/billing/checkout-session');
576
+ const req = createMockReq(JSON.stringify({
577
+ planVersionId: 'collab-v1',
578
+ interval: 'month'
579
+ }), {
580
+ 'x-taskforce-runtime-mode': 'cloud',
581
+ host: 'app.taskforcehq.ai'
582
+ });
583
+ const res = createMockRes();
584
+ await checkoutHandler(req, res, {});
585
+ expect(res.statusCode).toBe(200);
586
+ const args = mockCheckoutCreate.mock.calls.at(-1)?.[0];
587
+ expect(args.customer).toBe('cus_email_match');
588
+ expect(args.customer_email).toBeUndefined();
589
+ });
590
+ it('builds checkout return URLs from explicit local proxy origin headers', async () => {
591
+ const now = new Date().toISOString();
592
+ db.prepare(`
593
+ INSERT INTO plan_versions (plan_version_id, tenant_id, plan_id, version_number, is_default, metadata_json, created_at, updated_at)
594
+ VALUES ('collab-v1', 'default', 'collab', 1, 1, '{}', ?, ?)
595
+ `).run(now, now);
596
+ db.prepare(`
597
+ INSERT INTO billing_price_mappings (tenant_id, plan_version_id, billing_interval, stripe_price_id, active, created_at, updated_at)
598
+ VALUES ('default', 'collab-v1', 'month', 'price_collab_month', 1, ?, ?)
599
+ `).run(now, now);
600
+ const checkoutHandler = routeHandlers.get('POST /api/taskforce/billing/checkout-session');
601
+ const req = createMockReq(JSON.stringify({
602
+ planVersionId: 'collab-v1',
603
+ interval: 'month'
604
+ }), {
605
+ 'x-taskforce-runtime-mode': 'cloud',
606
+ host: 'performance-app.taskforcehq.ai',
607
+ 'x-taskforce-origin-host': 'localhost:5174',
608
+ 'x-taskforce-origin-proto': 'http',
609
+ 'x-taskforce-origin-base-url': 'http://localhost:5174',
610
+ 'x-taskforce-origin-runtime': 'local-proxy',
611
+ 'x-forwarded-host': 'localhost:5174',
612
+ 'x-forwarded-proto': 'http'
613
+ });
614
+ const res = createMockRes();
615
+ await checkoutHandler(req, res, {});
616
+ expect(res.statusCode).toBe(200);
617
+ const args = mockCheckoutCreate.mock.calls.at(-1)?.[0];
618
+ expect(String(args.success_url || '')).toContain('http://localhost:5174/?screen=plans&checkout=success');
619
+ expect(String(args.cancel_url || '')).toContain('http://localhost:5174/?screen=plans&checkout=cancel');
620
+ expect(JSON.parse(res.body)).toEqual(expect.objectContaining({
621
+ checkoutState: 'checkout',
622
+ returnBaseUrl: 'http://localhost:5174'
623
+ }));
624
+ });
625
+ it('prefers explicit proxy-origin headers over generic forwarded host values', async () => {
626
+ const now = new Date().toISOString();
627
+ db.prepare(`
628
+ INSERT INTO plan_versions (plan_version_id, tenant_id, plan_id, version_number, is_default, metadata_json, created_at, updated_at)
629
+ VALUES ('collab-v1', 'default', 'collab', 1, 1, '{}', ?, ?)
630
+ `).run(now, now);
631
+ db.prepare(`
632
+ INSERT INTO billing_price_mappings (tenant_id, plan_version_id, billing_interval, stripe_price_id, active, created_at, updated_at)
633
+ VALUES ('default', 'collab-v1', 'month', 'price_collab_month', 1, ?, ?)
634
+ `).run(now, now);
635
+ const checkoutHandler = routeHandlers.get('POST /api/taskforce/billing/checkout-session');
636
+ const req = createMockReq(JSON.stringify({
637
+ planVersionId: 'collab-v1',
638
+ interval: 'month'
639
+ }), {
640
+ 'x-taskforce-runtime-mode': 'cloud',
641
+ host: 'performance-app.taskforcehq.ai',
642
+ 'x-taskforce-origin-host': 'localhost:5174',
643
+ 'x-taskforce-origin-proto': 'http',
644
+ 'x-taskforce-origin-runtime': 'local-proxy',
645
+ 'x-forwarded-host': 'performance-app.taskforcehq.ai',
646
+ 'x-forwarded-proto': 'https'
647
+ });
648
+ const res = createMockRes();
649
+ await checkoutHandler(req, res, {});
650
+ expect(res.statusCode).toBe(200);
651
+ const args = mockCheckoutCreate.mock.calls.at(-1)?.[0];
652
+ expect(String(args.success_url || '')).toContain('http://localhost:5174/?screen=plans&checkout=success');
653
+ });
654
+ it('builds direct cloud checkout return URLs from the actual request protocol when forwarded proto is absent', async () => {
655
+ const now = new Date().toISOString();
656
+ db.prepare(`
657
+ INSERT INTO plan_versions (plan_version_id, tenant_id, plan_id, version_number, is_default, metadata_json, created_at, updated_at)
658
+ VALUES ('collab-v1', 'default', 'collab', 1, 1, '{}', ?, ?)
659
+ `).run(now, now);
660
+ db.prepare(`
661
+ INSERT INTO billing_price_mappings (tenant_id, plan_version_id, billing_interval, stripe_price_id, active, created_at, updated_at)
662
+ VALUES ('default', 'collab-v1', 'month', 'price_collab_month', 1, ?, ?)
663
+ `).run(now, now);
664
+ const checkoutHandler = routeHandlers.get('POST /api/taskforce/billing/checkout-session');
665
+ const req = createMockReq(JSON.stringify({
666
+ planVersionId: 'collab-v1',
667
+ interval: 'month'
668
+ }), {
669
+ 'x-taskforce-runtime-mode': 'cloud',
670
+ host: '127.0.0.1:4173'
671
+ });
672
+ req.socket = { encrypted: false };
673
+ const res = createMockRes();
674
+ await checkoutHandler(req, res, {});
675
+ expect(res.statusCode).toBe(200);
676
+ const args = mockCheckoutCreate.mock.calls.at(-1)?.[0];
677
+ expect(String(args.success_url || '')).toContain('http://127.0.0.1:4173/?screen=plans&checkout=success');
678
+ expect(String(args.cancel_url || '')).toContain('http://127.0.0.1:4173/?screen=plans&checkout=cancel');
679
+ });
680
+ it('applies a free plan immediately without creating a Stripe checkout session', async () => {
681
+ const now = new Date().toISOString();
682
+ db.prepare(`
683
+ INSERT INTO plan_versions (plan_version_id, tenant_id, plan_id, version_number, is_default, metadata_json, created_at, updated_at)
684
+ VALUES ('starter-v1', 'default', 'starter', 1, 1, '{}', ?, ?)
685
+ `).run(now, now);
686
+ db.prepare(`
687
+ 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)
688
+ VALUES ('default', 'starter-v1', 'month', 'free:starter-v1:month', 'free', 1, 0, NULL, ?, ?)
689
+ `).run(now, now);
690
+ const checkoutHandler = routeHandlers.get('POST /api/taskforce/billing/checkout-session');
691
+ const req = createMockReq(JSON.stringify({
692
+ planVersionId: 'starter-v1',
693
+ interval: 'month'
694
+ }), {
695
+ 'x-taskforce-runtime-mode': 'cloud',
696
+ host: 'app.taskforcehq.ai'
697
+ });
698
+ const res = createMockRes();
699
+ await checkoutHandler(req, res, {});
700
+ expect(res.statusCode).toBe(200);
701
+ expect(mockCheckoutCreate).not.toHaveBeenCalled();
702
+ expect(res.body).toContain('"checkoutState":"free"');
703
+ });
704
+ it('falls back to stripe email collection when user identity email is unavailable', async () => {
705
+ routeHandlers = buildBillingRouteHandlers(db, { userIdentity: { userId: 'u1', email: null, systemRole: 'user', betaAccess: true } });
706
+ const now = new Date().toISOString();
707
+ db.prepare(`
708
+ INSERT INTO plan_versions (plan_version_id, tenant_id, plan_id, version_number, is_default, metadata_json, created_at, updated_at)
709
+ VALUES ('collab-v1', 'default', 'collab', 1, 1, '{}', ?, ?)
710
+ `).run(now, now);
711
+ db.prepare(`
712
+ INSERT INTO billing_price_mappings (tenant_id, plan_version_id, billing_interval, stripe_price_id, active, created_at, updated_at)
713
+ VALUES ('default', 'collab-v1', 'month', 'price_collab_month', 1, ?, ?)
714
+ `).run(now, now);
715
+ const checkoutHandler = routeHandlers.get('POST /api/taskforce/billing/checkout-session');
716
+ const req = createMockReq(JSON.stringify({
717
+ planVersionId: 'collab-v1',
718
+ interval: 'month'
719
+ }), {
720
+ 'x-taskforce-runtime-mode': 'cloud',
721
+ host: 'app.taskforcehq.ai'
722
+ });
723
+ const res = createMockRes();
724
+ await checkoutHandler(req, res, {});
725
+ expect(res.statusCode).toBe(200);
726
+ const args = mockCheckoutCreate.mock.calls.at(-1)?.[0];
727
+ expect(args.customer).toBeUndefined();
728
+ expect(args.customer_email).toBeUndefined();
729
+ });
730
+ it('returns a clear error when the saved stripe customer no longer exists', async () => {
731
+ const now = new Date().toISOString();
732
+ db.prepare(`
733
+ INSERT INTO plan_versions (plan_version_id, tenant_id, plan_id, version_number, is_default, metadata_json, created_at, updated_at)
734
+ VALUES ('collab-v1', 'default', 'collab', 1, 1, '{}', ?, ?)
735
+ `).run(now, now);
736
+ db.prepare(`
737
+ INSERT INTO billing_price_mappings (tenant_id, plan_version_id, billing_interval, stripe_price_id, active, created_at, updated_at)
738
+ VALUES ('default', 'collab-v1', 'month', 'price_collab_month', 1, ?, ?)
739
+ `).run(now, now);
740
+ db.prepare(`
741
+ INSERT INTO user_billing (user_id, tenant_id, stripe_customer_id, entitlement_state, created_at, updated_at)
742
+ VALUES ('u1', 'default', 'cus_missing', 'active', ?, ?)
743
+ `).run(now, now);
744
+ mockCheckoutCreate.mockImplementationOnce(async () => {
745
+ const error = new Error('No such customer: cus_missing');
746
+ error.code = 'resource_missing';
747
+ throw error;
748
+ });
749
+ const checkoutHandler = routeHandlers.get('POST /api/taskforce/billing/checkout-session');
750
+ const req = createMockReq(JSON.stringify({
751
+ planVersionId: 'collab-v1',
752
+ interval: 'month'
753
+ }), {
754
+ 'x-taskforce-runtime-mode': 'cloud',
755
+ host: 'app.taskforcehq.ai'
756
+ });
757
+ const res = createMockRes();
758
+ await checkoutHandler(req, res, {});
759
+ expect(res.statusCode).toBe(409);
760
+ expect(JSON.parse(res.body)).toEqual({
761
+ error: 'The saved Stripe customer for this account could not be found. Please contact support before retrying checkout.',
762
+ code: 'BILLING_STRIPE_CUSTOMER_MISSING'
763
+ });
764
+ });
765
+ it('updates existing subscriptions without creating a checkout session', async () => {
766
+ const now = new Date().toISOString();
767
+ db.prepare(`
768
+ INSERT INTO plan_versions (plan_version_id, tenant_id, plan_id, version_number, is_default, metadata_json, created_at, updated_at)
769
+ VALUES ('pro-v2', 'default', 'pro', 2, 1, '{}', ?, ?)
770
+ `).run(now, now);
771
+ db.prepare(`
772
+ INSERT INTO billing_price_mappings (tenant_id, plan_version_id, billing_interval, stripe_price_id, active, created_at, updated_at)
773
+ VALUES ('default', 'pro-v2', 'year', 'price_pro_year', 1, ?, ?)
774
+ `).run(now, now);
775
+ db.prepare(`
776
+ INSERT INTO user_billing (
777
+ user_id, tenant_id, stripe_customer_id, stripe_subscription_id, stripe_price_id, billing_interval, entitlement_state, created_at, updated_at
778
+ ) VALUES ('u1', 'default', 'cus_existing', 'sub_existing', 'price_pro_month', 'month', 'active', ?, ?)
779
+ `).run(now, now);
780
+ db.prepare(`
781
+ INSERT INTO account_entitlements (
782
+ account_id, tenant_id, plan_version_id, entitlement_state, created_at, updated_at
783
+ ) VALUES ('u1', 'default', 'starter-v1', 'active', ?, ?)
784
+ `).run(now, now);
785
+ const checkoutHandler = routeHandlers.get('POST /api/taskforce/billing/checkout-session');
786
+ const req = createMockReq(JSON.stringify({
787
+ planVersionId: 'pro-v2',
788
+ interval: 'year'
789
+ }), {
790
+ 'x-taskforce-runtime-mode': 'cloud',
791
+ host: 'app.taskforcehq.ai'
792
+ });
793
+ const res = createMockRes();
794
+ await checkoutHandler(req, res, {});
795
+ expect(res.statusCode).toBe(200);
796
+ expect(mockSubscriptionUpdate).toHaveBeenCalledWith('sub_existing', expect.objectContaining({
797
+ items: [{ id: 'si_123', price: 'price_pro_year' }]
798
+ }));
799
+ expect(mockCheckoutCreate).not.toHaveBeenCalled();
800
+ expect(JSON.parse(res.body)).toEqual(expect.objectContaining({
801
+ checkoutState: 'updated'
802
+ }));
803
+ });
804
+ it('does not create a second checkout session for the same paid plan', async () => {
805
+ const now = new Date().toISOString();
806
+ db.prepare(`
807
+ INSERT INTO plan_versions (plan_version_id, tenant_id, plan_id, version_number, is_default, metadata_json, created_at, updated_at)
808
+ VALUES ('pro-v1', 'default', 'pro', 1, 1, '{}', ?, ?)
809
+ `).run(now, now);
810
+ db.prepare(`
811
+ INSERT INTO billing_price_mappings (tenant_id, plan_version_id, billing_interval, stripe_price_id, active, created_at, updated_at)
812
+ VALUES ('default', 'pro-v1', 'month', 'price_pro_month', 1, ?, ?)
813
+ `).run(now, now);
814
+ db.prepare(`
815
+ INSERT INTO user_billing (
816
+ user_id, tenant_id, stripe_customer_id, stripe_subscription_id, stripe_price_id, stripe_status, tier, billing_interval, entitlement_state, created_at, updated_at
817
+ ) VALUES ('u1', 'default', 'cus_123', 'sub_123', 'price_pro_month', 'active', 'pro', 'month', 'active', ?, ?)
818
+ `).run(now, now);
819
+ db.prepare(`
820
+ INSERT INTO account_entitlements (
821
+ account_id, tenant_id, plan_version_id, entitlement_state, effective_at, created_at, updated_at
822
+ ) VALUES ('u1', 'default', 'pro-v1', 'active', ?, ?, ?)
823
+ `).run(now, now, now);
824
+ mockSubscriptionList.mockResolvedValueOnce({
825
+ data: [{ id: 'sub_123', status: 'active' }]
826
+ });
827
+ const checkoutHandler = routeHandlers.get('POST /api/taskforce/billing/checkout-session');
828
+ const req = createMockReq(JSON.stringify({
829
+ planVersionId: 'pro-v1',
830
+ interval: 'month'
831
+ }), {
832
+ 'x-taskforce-runtime-mode': 'cloud',
833
+ host: 'app.taskforcehq.ai'
834
+ });
835
+ const res = createMockRes();
836
+ await checkoutHandler(req, res, {});
837
+ expect(res.statusCode).toBe(200);
838
+ expect(mockCheckoutCreate).not.toHaveBeenCalled();
839
+ expect(mockSubscriptionUpdate).not.toHaveBeenCalled();
840
+ expect(JSON.parse(res.body)).toEqual(expect.objectContaining({
841
+ checkoutState: 'already_current_plan',
842
+ planVersionId: 'pro-v1'
843
+ }));
844
+ });
845
+ it('discovers an active paid subscription by account email and blocks a duplicate purchase', async () => {
846
+ const now = new Date().toISOString();
847
+ db.prepare(`
848
+ INSERT INTO plan_versions (plan_version_id, tenant_id, plan_id, version_number, is_default, metadata_json, created_at, updated_at)
849
+ VALUES ('pro-v1', 'default', 'pro', 1, 1, '{}', ?, ?)
850
+ `).run(now, now);
851
+ db.prepare(`
852
+ INSERT INTO billing_price_mappings (tenant_id, plan_version_id, billing_interval, stripe_price_id, active, created_at, updated_at)
853
+ VALUES ('default', 'pro-v1', 'month', 'price_pro_month', 1, ?, ?)
854
+ `).run(now, now);
855
+ mockCustomersList.mockResolvedValueOnce({
856
+ data: [{ id: 'cus_email_match' }]
857
+ });
858
+ mockSubscriptionList.mockResolvedValueOnce({
859
+ data: [{
860
+ id: 'sub_email_match',
861
+ customer: 'cus_email_match',
862
+ status: 'active',
863
+ trial_end: null,
864
+ metadata: { userId: 'u1', planVersionId: 'pro-v1' },
865
+ items: {
866
+ data: [{
867
+ id: 'si_email_match',
868
+ current_period_end: Math.floor(Date.now() / 1000) + 86400,
869
+ price: { id: 'price_pro_month', recurring: { interval: 'month' } }
870
+ }]
871
+ }
872
+ }]
873
+ });
874
+ const checkoutHandler = routeHandlers.get('POST /api/taskforce/billing/checkout-session');
875
+ const req = createMockReq(JSON.stringify({
876
+ planVersionId: 'pro-v1',
877
+ interval: 'month'
878
+ }), {
879
+ 'x-taskforce-runtime-mode': 'cloud',
880
+ host: 'app.taskforcehq.ai'
881
+ });
882
+ const res = createMockRes();
883
+ await checkoutHandler(req, res, {});
884
+ expect(res.statusCode).toBe(200);
885
+ expect(mockCheckoutCreate).not.toHaveBeenCalled();
886
+ expect(JSON.parse(res.body)).toEqual(expect.objectContaining({
887
+ checkoutState: 'already_current_plan',
888
+ planVersionId: 'pro-v1'
889
+ }));
890
+ const mirrored = db.prepare(`
891
+ SELECT stripe_customer_id, stripe_subscription_id
892
+ FROM user_billing
893
+ WHERE tenant_id = 'default' AND user_id = 'u1'
894
+ LIMIT 1
895
+ `).get();
896
+ expect(mirrored).toEqual(expect.objectContaining({
897
+ stripe_customer_id: 'cus_email_match',
898
+ stripe_subscription_id: 'sub_email_match'
899
+ }));
900
+ });
901
+ it('updates the existing paid subscription instead of creating a second checkout', async () => {
902
+ const now = new Date().toISOString();
903
+ db.prepare(`
904
+ INSERT INTO plan_versions (plan_version_id, tenant_id, plan_id, version_number, is_default, metadata_json, created_at, updated_at)
905
+ VALUES ('pro-v1', 'default', 'pro', 1, 1, '{}', ?, ?)
906
+ `).run(now, now);
907
+ db.prepare(`
908
+ INSERT INTO plan_versions (plan_version_id, tenant_id, plan_id, version_number, is_default, metadata_json, created_at, updated_at)
909
+ VALUES ('team-v1', 'default', 'team', 1, 1, '{}', ?, ?)
910
+ `).run(now, now);
911
+ db.prepare(`
912
+ INSERT INTO billing_price_mappings (tenant_id, plan_version_id, billing_interval, stripe_price_id, active, created_at, updated_at)
913
+ VALUES ('default', 'team-v1', 'month', 'price_team_month', 1, ?, ?)
914
+ `).run(now, now);
915
+ db.prepare(`
916
+ INSERT INTO user_billing (
917
+ user_id, tenant_id, stripe_customer_id, stripe_subscription_id, stripe_price_id, stripe_status, tier, billing_interval, entitlement_state, created_at, updated_at
918
+ ) VALUES ('u1', 'default', 'cus_123', 'sub_123', 'price_pro_month', 'active', 'pro', 'month', 'active', ?, ?)
919
+ `).run(now, now);
920
+ db.prepare(`
921
+ INSERT INTO account_entitlements (
922
+ account_id, tenant_id, plan_version_id, entitlement_state, effective_at, created_at, updated_at
923
+ ) VALUES ('u1', 'default', 'pro-v1', 'active', ?, ?, ?)
924
+ `).run(now, now, now);
925
+ mockSubscriptionList.mockResolvedValueOnce({
926
+ data: [{ id: 'sub_123', status: 'active' }]
927
+ });
928
+ mockSubscriptionUpdate.mockResolvedValueOnce({
929
+ id: 'sub_123',
930
+ customer: 'cus_123',
931
+ status: 'active',
932
+ trial_end: null,
933
+ metadata: { userId: 'u1', planVersionId: 'team-v1' },
934
+ items: {
935
+ data: [{
936
+ id: 'si_123',
937
+ current_period_end: Math.floor(Date.now() / 1000) + 86400,
938
+ price: { id: 'price_team_month', recurring: { interval: 'month' } }
939
+ }]
940
+ }
941
+ });
942
+ const checkoutHandler = routeHandlers.get('POST /api/taskforce/billing/checkout-session');
943
+ const req = createMockReq(JSON.stringify({
944
+ planVersionId: 'team-v1',
945
+ interval: 'month'
946
+ }), {
947
+ 'x-taskforce-runtime-mode': 'cloud',
948
+ host: 'app.taskforcehq.ai'
949
+ });
950
+ const res = createMockRes();
951
+ await checkoutHandler(req, res, {});
952
+ expect(res.statusCode).toBe(200);
953
+ expect(mockCheckoutCreate).not.toHaveBeenCalled();
954
+ expect(mockSubscriptionUpdate).toHaveBeenCalledWith('sub_123', expect.objectContaining({
955
+ items: [{ id: 'si_123', price: 'price_team_month' }]
956
+ }));
957
+ expect(JSON.parse(res.body)).toEqual(expect.objectContaining({
958
+ checkoutState: 'updated',
959
+ planVersionId: 'team-v1'
960
+ }));
961
+ const entitlement = db.prepare(`
962
+ SELECT plan_version_id
963
+ FROM account_entitlements
964
+ WHERE tenant_id = 'default' AND account_id = 'u1'
965
+ LIMIT 1
966
+ `).get();
967
+ expect(entitlement?.plan_version_id).toBe('team-v1');
968
+ });
969
+ it('updates the existing subscription when only the interval changes on the same plan', async () => {
970
+ const now = new Date().toISOString();
971
+ db.prepare(`
972
+ INSERT INTO plan_versions (plan_version_id, tenant_id, plan_id, version_number, is_default, metadata_json, created_at, updated_at)
973
+ VALUES ('pro-v1', 'default', 'pro', 1, 1, '{}', ?, ?)
974
+ `).run(now, now);
975
+ db.prepare(`
976
+ INSERT INTO billing_price_mappings (tenant_id, plan_version_id, billing_interval, stripe_price_id, active, created_at, updated_at)
977
+ VALUES ('default', 'pro-v1', 'month', 'price_pro_month', 1, ?, ?)
978
+ `).run(now, now);
979
+ db.prepare(`
980
+ INSERT INTO billing_price_mappings (tenant_id, plan_version_id, billing_interval, stripe_price_id, active, created_at, updated_at)
981
+ VALUES ('default', 'pro-v1', 'year', 'price_pro_year', 1, ?, ?)
982
+ `).run(now, now);
983
+ db.prepare(`
984
+ INSERT INTO user_billing (
985
+ user_id, tenant_id, stripe_customer_id, stripe_subscription_id, stripe_price_id, stripe_status, tier, billing_interval, entitlement_state, created_at, updated_at
986
+ ) VALUES ('u1', 'default', 'cus_123', 'sub_123', 'price_pro_month', 'active', 'pro', 'month', 'active', ?, ?)
987
+ `).run(now, now);
988
+ db.prepare(`
989
+ INSERT INTO account_entitlements (
990
+ account_id, tenant_id, plan_version_id, entitlement_state, effective_at, created_at, updated_at
991
+ ) VALUES ('u1', 'default', 'pro-v1', 'active', ?, ?, ?)
992
+ `).run(now, now, now);
993
+ mockSubscriptionList.mockResolvedValueOnce({
994
+ data: [{ id: 'sub_123', status: 'active' }]
995
+ });
996
+ mockSubscriptionUpdate.mockResolvedValueOnce({
997
+ id: 'sub_123',
998
+ customer: 'cus_123',
999
+ status: 'active',
1000
+ trial_end: null,
1001
+ metadata: { userId: 'u1', planVersionId: 'pro-v1' },
1002
+ items: {
1003
+ data: [{
1004
+ id: 'si_123',
1005
+ current_period_end: Math.floor(Date.now() / 1000) + 86400,
1006
+ price: { id: 'price_pro_year', recurring: { interval: 'year' } }
1007
+ }]
1008
+ }
1009
+ });
1010
+ const checkoutHandler = routeHandlers.get('POST /api/taskforce/billing/checkout-session');
1011
+ const req = createMockReq(JSON.stringify({
1012
+ planVersionId: 'pro-v1',
1013
+ interval: 'year'
1014
+ }), {
1015
+ 'x-taskforce-runtime-mode': 'cloud',
1016
+ host: 'app.taskforcehq.ai'
1017
+ });
1018
+ const res = createMockRes();
1019
+ await checkoutHandler(req, res, {});
1020
+ expect(res.statusCode).toBe(200);
1021
+ expect(mockCheckoutCreate).not.toHaveBeenCalled();
1022
+ expect(mockSubscriptionUpdate).toHaveBeenCalledWith('sub_123', expect.objectContaining({
1023
+ items: [{ id: 'si_123', price: 'price_pro_year' }]
1024
+ }));
1025
+ expect(JSON.parse(res.body)).toEqual(expect.objectContaining({
1026
+ checkoutState: 'updated',
1027
+ planVersionId: 'pro-v1'
1028
+ }));
1029
+ });
1030
+ it('confirms a successful checkout session and updates the entitlement immediately', async () => {
1031
+ const now = new Date().toISOString();
1032
+ db.prepare(`
1033
+ INSERT INTO plan_versions (plan_version_id, tenant_id, plan_id, version_number, is_default, metadata_json, created_at, updated_at)
1034
+ VALUES ('pro-v1', 'default', 'pro', 1, 1, '{}', ?, ?)
1035
+ `).run(now, now);
1036
+ db.prepare(`
1037
+ INSERT INTO billing_price_mappings (tenant_id, plan_version_id, billing_interval, stripe_price_id, active, created_at, updated_at)
1038
+ VALUES ('default', 'pro-v1', 'month', 'price_pro_month', 1, ?, ?)
1039
+ `).run(now, now);
1040
+ const confirmHandler = routeHandlers.get('POST /api/taskforce/billing/checkout-session/confirm');
1041
+ expect(confirmHandler).toBeTypeOf('function');
1042
+ const req = createMockReq(JSON.stringify({
1043
+ sessionId: 'cs_success_123'
1044
+ }), {
1045
+ 'x-taskforce-runtime-mode': 'cloud',
1046
+ host: 'app.taskforcehq.ai'
1047
+ });
1048
+ const res = createMockRes();
1049
+ await confirmHandler(req, res, {});
1050
+ expect(res.statusCode).toBe(200);
1051
+ expect(mockCheckoutRetrieve).toHaveBeenCalledWith('cs_success_123', expect.objectContaining({
1052
+ expand: ['subscription']
1053
+ }));
1054
+ expect(mockSubscriptionRetrieve).toHaveBeenCalledWith('sub_123');
1055
+ const entitlement = db.prepare(`
1056
+ SELECT plan_version_id, entitlement_state
1057
+ FROM account_entitlements
1058
+ WHERE tenant_id = 'default' AND account_id = 'u1'
1059
+ LIMIT 1
1060
+ `).get();
1061
+ expect(entitlement).toEqual(expect.objectContaining({
1062
+ plan_version_id: 'pro-v1',
1063
+ entitlement_state: 'active'
1064
+ }));
1065
+ });
1066
+ it('lists webhook failure diagnostics for system admin', async () => {
1067
+ const now = new Date().toISOString();
1068
+ const old = new Date(Date.now() - 48 * 60 * 60 * 1000).toISOString();
1069
+ db.prepare(`
1070
+ INSERT INTO stripe_webhook_failures (id, tenant_id, event_id, event_type, reason, payload_excerpt, purge_after, created_at)
1071
+ VALUES ('whf_keep_1', 'default', 'evt_1', 'invoice.paid', 'diagnostic-1', '{"id":"evt_1"}', ?, ?)
1072
+ `).run(new Date(Date.now() + 48 * 60 * 60 * 1000).toISOString(), now);
1073
+ db.prepare(`
1074
+ INSERT INTO stripe_webhook_failures (id, tenant_id, event_id, event_type, reason, payload_excerpt, purge_after, created_at)
1075
+ VALUES ('whf_purge_1', 'default', 'evt_old', 'invoice.paid', 'diagnostic-old', '{"id":"evt_old"}', ?, ?)
1076
+ `).run(old, now);
1077
+ const route = routeHandlers.get('GET /api/taskforce/admin/billing/webhook-failures');
1078
+ expect(route).toBeTypeOf('function');
1079
+ const req = createMockReq('', {
1080
+ 'x-taskforce-runtime-mode': 'cloud'
1081
+ });
1082
+ req.url = '/api/taskforce/admin/billing/webhook-failures?limit=10';
1083
+ const res = createMockRes();
1084
+ await route(req, res, {});
1085
+ expect(res.statusCode).toBe(200);
1086
+ const payload = JSON.parse(res.body);
1087
+ expect(payload.purgedExpiredRows).toBeGreaterThanOrEqual(1);
1088
+ expect(payload.total).toBeGreaterThanOrEqual(1);
1089
+ expect(Array.isArray(payload.rows)).toBe(true);
1090
+ expect(payload.rows[0]).toEqual(expect.objectContaining({
1091
+ event_id: 'evt_1',
1092
+ reason: 'diagnostic-1'
1093
+ }));
1094
+ });
1095
+ it('lists stripe price options filtered by interval for system admin', async () => {
1096
+ mockPricesList.mockResolvedValueOnce({
1097
+ data: [
1098
+ { id: 'price_month_1', active: true, currency: 'usd', unit_amount: 1200, recurring: { interval: 'month' }, nickname: 'Monthly', product: 'prod_1' },
1099
+ { id: 'price_year_1', active: true, currency: 'usd', unit_amount: 12000, recurring: { interval: 'year' }, nickname: 'Yearly', product: 'prod_1' }
1100
+ ],
1101
+ has_more: false
1102
+ });
1103
+ const route = routeHandlers.get('GET /api/taskforce/admin/billing/stripe-price-options');
1104
+ expect(route).toBeTypeOf('function');
1105
+ const req = createMockReq('', {
1106
+ 'x-taskforce-runtime-mode': 'cloud'
1107
+ });
1108
+ req.url = '/api/taskforce/admin/billing/stripe-price-options?interval=month';
1109
+ const res = createMockRes();
1110
+ await route(req, res, {});
1111
+ expect(res.statusCode).toBe(200);
1112
+ const body = JSON.parse(res.body);
1113
+ expect(Array.isArray(body.options)).toBe(true);
1114
+ expect(body.options.length).toBe(1);
1115
+ expect(body.options[0]).toEqual(expect.objectContaining({
1116
+ stripePriceId: 'price_month_1',
1117
+ interval: 'month'
1118
+ }));
1119
+ });
1120
+ it('keeps active manual price mappings during Stripe import when the price is still active but lacks plan metadata', async () => {
1121
+ const now = new Date().toISOString();
1122
+ db.prepare(`
1123
+ INSERT INTO plan_versions (plan_version_id, tenant_id, plan_id, version_number, is_default, metadata_json, created_at, updated_at)
1124
+ VALUES ('collab-v1', 'default', 'collab', 1, 1, '{}', ?, ?)
1125
+ `).run(now, now);
1126
+ db.prepare(`
1127
+ 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)
1128
+ VALUES ('default', 'collab-v1', 'month', 'price_manual', 'stripe', 1, 1900, 'usd', ?, ?)
1129
+ `).run(now, now);
1130
+ mockPricesList.mockResolvedValueOnce({
1131
+ data: [
1132
+ {
1133
+ id: 'price_manual',
1134
+ active: true,
1135
+ currency: 'usd',
1136
+ unit_amount: 1900,
1137
+ recurring: { interval: 'month' },
1138
+ metadata: {}
1139
+ }
1140
+ ],
1141
+ has_more: false
1142
+ });
1143
+ const route = routeHandlers.get('POST /api/taskforce/admin/billing/prices/import');
1144
+ expect(route).toBeTypeOf('function');
1145
+ const req = createMockReq('', {
1146
+ 'x-taskforce-runtime-mode': 'cloud'
1147
+ });
1148
+ const res = createMockRes();
1149
+ await route(req, res, {});
1150
+ expect(res.statusCode).toBe(200);
1151
+ const row = db.prepare(`
1152
+ SELECT active
1153
+ FROM billing_price_mappings
1154
+ WHERE tenant_id = 'default' AND stripe_price_id = 'price_manual'
1155
+ LIMIT 1
1156
+ `).get();
1157
+ expect(row.active).toBe(1);
1158
+ });
1159
+ it('upserts price mappings and deactivates prior active mapping for same version interval', async () => {
1160
+ const now = new Date().toISOString();
1161
+ db.prepare(`
1162
+ INSERT INTO plan_versions (plan_version_id, tenant_id, plan_id, version_number, is_default, metadata_json, created_at, updated_at)
1163
+ VALUES ('collab-v1', 'default', 'collab', 1, 1, '{}', ?, ?)
1164
+ `).run(now, now);
1165
+ db.prepare(`
1166
+ 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)
1167
+ VALUES ('default', 'collab-v1', 'month', 'price_old', 'stripe', 1, 1100, 'usd', ?, ?)
1168
+ `).run(now, now);
1169
+ mockPriceRetrieve.mockResolvedValueOnce({
1170
+ id: 'price_new',
1171
+ active: true,
1172
+ currency: 'usd',
1173
+ unit_amount: 1500,
1174
+ recurring: { interval: 'month' }
1175
+ });
1176
+ const upsertRoute = routeHandlers.get('PUT /api/taskforce/admin/billing/price-mappings');
1177
+ expect(upsertRoute).toBeTypeOf('function');
1178
+ const upsertReq = createMockReq(JSON.stringify({
1179
+ planVersionId: 'collab-v1',
1180
+ interval: 'month',
1181
+ stripePriceId: 'price_new',
1182
+ active: true
1183
+ }), { 'x-taskforce-runtime-mode': 'cloud' });
1184
+ const upsertRes = createMockRes();
1185
+ await upsertRoute(upsertReq, upsertRes, {});
1186
+ expect(upsertRes.statusCode).toBe(200);
1187
+ const rows = db.prepare(`
1188
+ SELECT stripe_price_id, active
1189
+ FROM billing_price_mappings
1190
+ WHERE tenant_id = 'default' AND plan_version_id = 'collab-v1' AND billing_interval = 'month'
1191
+ ORDER BY stripe_price_id ASC
1192
+ `).all();
1193
+ expect(rows).toEqual([
1194
+ { stripe_price_id: 'price_new', active: 1 },
1195
+ { stripe_price_id: 'price_old', active: 0 }
1196
+ ]);
1197
+ });
1198
+ it('allows the same stripe price id to be assigned to multiple plan versions', async () => {
1199
+ const now = new Date().toISOString();
1200
+ db.prepare(`
1201
+ INSERT INTO plan_versions (plan_version_id, tenant_id, plan_id, version_number, is_default, metadata_json, created_at, updated_at)
1202
+ VALUES ('collab-v1', 'default', 'collab', 1, 1, '{}', ?, ?)
1203
+ `).run(now, now);
1204
+ db.prepare(`
1205
+ INSERT INTO plan_versions (plan_version_id, tenant_id, plan_id, version_number, is_default, metadata_json, created_at, updated_at)
1206
+ VALUES ('team-v1', 'default', 'team', 1, 1, '{}', ?, ?)
1207
+ `).run(now, now);
1208
+ mockPriceRetrieve.mockResolvedValue({
1209
+ id: 'price_shared',
1210
+ active: true,
1211
+ currency: 'usd',
1212
+ unit_amount: 1900,
1213
+ recurring: { interval: 'month' }
1214
+ });
1215
+ const upsertRoute = routeHandlers.get('PUT /api/taskforce/admin/billing/price-mappings');
1216
+ const firstReq = createMockReq(JSON.stringify({
1217
+ planVersionId: 'collab-v1',
1218
+ interval: 'month',
1219
+ stripePriceId: 'price_shared',
1220
+ active: true
1221
+ }), { 'x-taskforce-runtime-mode': 'cloud' });
1222
+ const firstRes = createMockRes();
1223
+ await upsertRoute(firstReq, firstRes, {});
1224
+ expect(firstRes.statusCode).toBe(200);
1225
+ const secondReq = createMockReq(JSON.stringify({
1226
+ planVersionId: 'team-v1',
1227
+ interval: 'month',
1228
+ stripePriceId: 'price_shared',
1229
+ active: true
1230
+ }), { 'x-taskforce-runtime-mode': 'cloud' });
1231
+ const secondRes = createMockRes();
1232
+ await upsertRoute(secondReq, secondRes, {});
1233
+ expect(secondRes.statusCode).toBe(200);
1234
+ const rows = db.prepare(`
1235
+ SELECT plan_version_id, stripe_price_id, active
1236
+ FROM billing_price_mappings
1237
+ WHERE tenant_id = 'default' AND stripe_price_id = 'price_shared'
1238
+ ORDER BY plan_version_id ASC
1239
+ `).all();
1240
+ expect(rows).toEqual([
1241
+ { plan_version_id: 'collab-v1', stripe_price_id: 'price_shared', active: 1 },
1242
+ { plan_version_id: 'team-v1', stripe_price_id: 'price_shared', active: 1 }
1243
+ ]);
1244
+ });
1245
+ it('keeps reused mappings for other plan versions during Stripe import', async () => {
1246
+ const now = new Date().toISOString();
1247
+ db.prepare(`
1248
+ INSERT INTO plan_versions (plan_version_id, tenant_id, plan_id, version_number, is_default, metadata_json, created_at, updated_at)
1249
+ VALUES ('collab-v1', 'default', 'collab', 1, 1, '{}', ?, ?)
1250
+ `).run(now, now);
1251
+ db.prepare(`
1252
+ INSERT INTO plan_versions (plan_version_id, tenant_id, plan_id, version_number, is_default, metadata_json, created_at, updated_at)
1253
+ VALUES ('team-v1', 'default', 'team', 1, 1, '{}', ?, ?)
1254
+ `).run(now, now);
1255
+ db.prepare(`
1256
+ 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)
1257
+ VALUES ('default', 'collab-v1', 'month', 'price_shared', 'stripe', 1, 1900, 'usd', ?, ?)
1258
+ `).run(now, now);
1259
+ db.prepare(`
1260
+ 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)
1261
+ VALUES ('default', 'team-v1', 'month', 'price_shared', 'stripe', 1, 1900, 'usd', ?, ?)
1262
+ `).run(now, now);
1263
+ mockPricesList.mockResolvedValueOnce({
1264
+ data: [
1265
+ {
1266
+ id: 'price_shared',
1267
+ active: true,
1268
+ currency: 'usd',
1269
+ unit_amount: 1900,
1270
+ recurring: { interval: 'month' },
1271
+ metadata: { taskforce_plan_version_id: 'collab-v1' }
1272
+ }
1273
+ ],
1274
+ has_more: false
1275
+ });
1276
+ const route = routeHandlers.get('POST /api/taskforce/admin/billing/prices/import');
1277
+ const req = createMockReq('', { 'x-taskforce-runtime-mode': 'cloud' });
1278
+ const res = createMockRes();
1279
+ await route(req, res, {});
1280
+ expect(res.statusCode).toBe(200);
1281
+ const rows = db.prepare(`
1282
+ SELECT plan_version_id, stripe_price_id, active
1283
+ FROM billing_price_mappings
1284
+ WHERE tenant_id = 'default' AND stripe_price_id = 'price_shared'
1285
+ ORDER BY plan_version_id ASC
1286
+ `).all();
1287
+ expect(rows).toEqual([
1288
+ { plan_version_id: 'collab-v1', stripe_price_id: 'price_shared', active: 1 },
1289
+ { plan_version_id: 'team-v1', stripe_price_id: 'price_shared', active: 1 }
1290
+ ]);
1291
+ });
1292
+ it('upserts explicit free pricing without requiring a Stripe price id', async () => {
1293
+ const now = new Date().toISOString();
1294
+ db.prepare(`
1295
+ INSERT INTO plan_versions (plan_version_id, tenant_id, plan_id, version_number, is_default, metadata_json, created_at, updated_at)
1296
+ VALUES ('starter-v1', 'default', 'starter', 1, 1, '{}', ?, ?)
1297
+ `).run(now, now);
1298
+ const upsertRoute = routeHandlers.get('PUT /api/taskforce/admin/billing/price-mappings');
1299
+ expect(upsertRoute).toBeTypeOf('function');
1300
+ const upsertReq = createMockReq(JSON.stringify({
1301
+ planVersionId: 'starter-v1',
1302
+ interval: 'month',
1303
+ pricingType: 'free',
1304
+ active: true
1305
+ }), { 'x-taskforce-runtime-mode': 'cloud' });
1306
+ const upsertRes = createMockRes();
1307
+ await upsertRoute(upsertReq, upsertRes, {});
1308
+ expect(upsertRes.statusCode).toBe(200);
1309
+ const row = db.prepare(`
1310
+ SELECT stripe_price_id, pricing_type, active, unit_amount
1311
+ FROM billing_price_mappings
1312
+ WHERE tenant_id = 'default' AND plan_version_id = 'starter-v1' AND billing_interval = 'month'
1313
+ LIMIT 1
1314
+ `).get();
1315
+ expect(row.stripe_price_id).toBe('free:starter-v1:month');
1316
+ expect(row.pricing_type).toBe('free');
1317
+ expect(row.active).toBe(1);
1318
+ expect(row.unit_amount).toBe(0);
1319
+ });
1320
+ it('rejects webhook failure diagnostics for non-admin', async () => {
1321
+ const nonAdminHandlers = buildBillingRouteHandlers(db, { isAdmin: false });
1322
+ const route = nonAdminHandlers.get('GET /api/taskforce/admin/billing/webhook-failures');
1323
+ expect(route).toBeTypeOf('function');
1324
+ const req = createMockReq('', {
1325
+ 'x-taskforce-runtime-mode': 'cloud'
1326
+ });
1327
+ req.url = '/api/taskforce/admin/billing/webhook-failures';
1328
+ const res = createMockRes();
1329
+ await route(req, res, {});
1330
+ expect(res.statusCode).toBe(403);
1331
+ });
1332
+ it('returns public pricing catalog with cached Stripe amounts without auth', async () => {
1333
+ const now = new Date().toISOString();
1334
+ routeHandlers = buildBillingRouteHandlers(db);
1335
+ const pricingRoute = routeHandlers.get('GET /api/taskforce/public/pricing');
1336
+ expect(pricingRoute).toBeTypeOf('function');
1337
+ db.prepare(`
1338
+ INSERT INTO plan_versions (plan_version_id, tenant_id, plan_id, version_number, is_default, metadata_json, created_at, updated_at)
1339
+ VALUES ('pro-v1', 'default', 'pro', 1, 1, '{}', ?, ?)
1340
+ `).run(now, now);
1341
+ db.prepare(`
1342
+ INSERT INTO plan_versions (plan_version_id, tenant_id, plan_id, version_number, is_default, metadata_json, created_at, updated_at)
1343
+ VALUES ('starter-v1', 'default', 'starter', 1, 1, '{}', ?, ?)
1344
+ `).run(now, now);
1345
+ db.prepare(`
1346
+ 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)
1347
+ VALUES ('default', 'pro-v1', 'month', 'price_pro_month', 'stripe', 1, 1500, 'usd', ?, ?)
1348
+ `).run(now, now);
1349
+ db.prepare(`
1350
+ 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)
1351
+ VALUES ('default', 'starter-v1', 'month', 'free:starter-v1:month', 'free', 1, 0, NULL, ?, ?)
1352
+ `).run(now, now);
1353
+ const req = createMockReq('', { 'x-taskforce-runtime-mode': 'cloud' });
1354
+ const res = createMockRes();
1355
+ await pricingRoute(req, res, {});
1356
+ expect(res.statusCode).toBe(200);
1357
+ const body = JSON.parse(res.body);
1358
+ expect(body.plans).toBeInstanceOf(Array);
1359
+ expect(body.pricingSource).toEqual({
1360
+ environment: 'production',
1361
+ runtimeMode: 'cloud'
1362
+ });
1363
+ // Only active plans with versions should be returned (pro, starter)
1364
+ // inactive-plan (archived) and empty-plan (no versions) are excluded.
1365
+ expect(body.plans.length).toBe(2);
1366
+ const proPlan = body.plans.find((p) => p.planId === 'pro');
1367
+ const freePlan = body.plans.find((p) => p.planId === 'starter');
1368
+ expect(proPlan).toBeDefined();
1369
+ expect(freePlan).toBeDefined();
1370
+ // Pro plan has mapped prices
1371
+ const monthPricing = proPlan.pricing.month;
1372
+ expect(monthPricing).not.toBeNull();
1373
+ expect(monthPricing.unitAmount).toBe(1500);
1374
+ expect(monthPricing.currency).toBe('usd');
1375
+ expect(monthPricing.stripePriceId).toBe('price_pro_month');
1376
+ expect(proPlan.pricing.year).toBeNull();
1377
+ // Free plan is explicitly free, not "missing"
1378
+ expect(freePlan.pricing.month.pricingType).toBe('free');
1379
+ expect(freePlan.pricing.month.unitAmount).toBe(0);
1380
+ expect(freePlan.pricing.month.stripePriceId).toBeNull();
1381
+ expect(freePlan.pricing.year).toBeNull();
1382
+ expect(proPlan.defaultVersion.trialDays).toBe(0);
1383
+ });
1384
+ it('returns the pricing environment derived from the cloud host', async () => {
1385
+ routeHandlers = buildBillingRouteHandlers(db);
1386
+ const pricingRoute = routeHandlers.get('GET /api/taskforce/public/pricing');
1387
+ expect(pricingRoute).toBeTypeOf('function');
1388
+ const req = createMockReq('', {
1389
+ 'x-taskforce-runtime-mode': 'cloud',
1390
+ host: 'performance-app.taskforcehq.ai'
1391
+ });
1392
+ const res = createMockRes();
1393
+ await pricingRoute(req, res, {});
1394
+ expect(res.statusCode).toBe(200);
1395
+ const body = JSON.parse(res.body);
1396
+ expect(body.pricingSource).toEqual({
1397
+ environment: 'performance',
1398
+ runtimeMode: 'cloud'
1399
+ });
1400
+ });
1401
+ it('rejects local runtime pricing requests so they cannot serve local catalog data', async () => {
1402
+ routeHandlers = buildBillingRouteHandlers(db);
1403
+ const pricingRoute = routeHandlers.get('GET /api/taskforce/public/pricing');
1404
+ expect(pricingRoute).toBeTypeOf('function');
1405
+ const req = createMockReq('', { 'x-taskforce-runtime-mode': 'local' });
1406
+ const res = createMockRes();
1407
+ await pricingRoute(req, res, {});
1408
+ expect(res.statusCode).toBe(500);
1409
+ expect(JSON.parse(res.body)).toEqual({
1410
+ error: 'Pricing must be served through the configured cloud pricing proxy in local runtime.'
1411
+ });
1412
+ });
1413
+ it('returns plan display name in billing status payload', async () => {
1414
+ const now = new Date().toISOString();
1415
+ db.prepare(`
1416
+ INSERT INTO plan_catalog (plan_id, tenant_id, display_name, status, created_at, updated_at)
1417
+ VALUES ('pro', 'default', 'Pro Plan', 'active', ?, ?)
1418
+ `).run(now, now);
1419
+ db.prepare(`
1420
+ INSERT INTO user_billing (
1421
+ user_id, tenant_id, stripe_customer_id, stripe_subscription_id, stripe_price_id, stripe_status, tier, billing_interval, entitlement_state, created_at, updated_at
1422
+ ) VALUES ('u1', 'default', 'cus_123', 'sub_123', 'price_123', 'active', 'pro', 'month', 'active', ?, ?)
1423
+ `).run(now, now);
1424
+ const statusRoute = routeHandlers.get('GET /api/taskforce/billing/status');
1425
+ expect(statusRoute).toBeTypeOf('function');
1426
+ const req = createMockReq('', { 'x-taskforce-runtime-mode': 'cloud' });
1427
+ const res = createMockRes();
1428
+ await statusRoute(req, res, {});
1429
+ expect(res.statusCode).toBe(200);
1430
+ const body = JSON.parse(res.body);
1431
+ expect(body.planId).toBe('pro');
1432
+ expect(body.planName).toBe('Pro Plan');
1433
+ expect(body.planVersionId).toBe('pro-v1');
1434
+ expect(body.entitlementState).toBe('active');
1435
+ });
1436
+ it('reconciles stale entitlement from Stripe when billing status is requested', async () => {
1437
+ const now = new Date().toISOString();
1438
+ db.prepare(`
1439
+ INSERT INTO plan_catalog (plan_id, tenant_id, display_name, status, created_at, updated_at)
1440
+ VALUES ('pro', 'default', 'Pro Plan', 'active', ?, ?)
1441
+ `).run(now, now);
1442
+ db.prepare(`
1443
+ INSERT INTO plan_versions (plan_version_id, tenant_id, plan_id, version_number, is_default, metadata_json, created_at, updated_at)
1444
+ VALUES ('pro-v1', 'default', 'pro', 1, 1, '{}', ?, ?)
1445
+ `).run(now, now);
1446
+ db.prepare(`
1447
+ INSERT INTO billing_price_mappings (tenant_id, plan_version_id, billing_interval, stripe_price_id, active, created_at, updated_at)
1448
+ VALUES ('default', 'pro-v1', 'month', 'price_pro_month', 1, ?, ?)
1449
+ `).run(now, now);
1450
+ db.prepare(`
1451
+ INSERT INTO user_billing (
1452
+ user_id, tenant_id, stripe_customer_id, stripe_subscription_id, stripe_price_id, stripe_status, tier, billing_interval, entitlement_state, created_at, updated_at
1453
+ ) VALUES ('u1', 'default', 'cus_123', 'sub_123', 'price_old', 'active', 'starter', 'month', 'active', ?, ?)
1454
+ `).run(now, now);
1455
+ db.prepare(`
1456
+ INSERT INTO account_entitlements (
1457
+ account_id, tenant_id, plan_version_id, entitlement_state, effective_at, created_at, updated_at
1458
+ ) VALUES ('u1', 'default', 'starter-v1', 'active', ?, ?, ?)
1459
+ `).run(now, now, now);
1460
+ const statusRoute = routeHandlers.get('GET /api/taskforce/billing/status');
1461
+ const req = createMockReq('', { 'x-taskforce-runtime-mode': 'cloud' });
1462
+ const res = createMockRes();
1463
+ await statusRoute(req, res, {});
1464
+ expect(res.statusCode).toBe(200);
1465
+ expect(mockSubscriptionRetrieve).toHaveBeenCalledWith('sub_123');
1466
+ const entitlement = db.prepare(`
1467
+ SELECT plan_version_id
1468
+ FROM account_entitlements
1469
+ WHERE tenant_id = 'default' AND account_id = 'u1'
1470
+ LIMIT 1
1471
+ `).get();
1472
+ expect(entitlement?.plan_version_id).toBe('pro-v1');
1473
+ });
1474
+ it('repairs billing status from an active Stripe subscription discovered by account email', async () => {
1475
+ const now = new Date().toISOString();
1476
+ db.prepare(`
1477
+ INSERT INTO plan_catalog (plan_id, tenant_id, display_name, status, created_at, updated_at)
1478
+ VALUES ('pro', 'default', 'Pro Plan', 'active', ?, ?)
1479
+ `).run(now, now);
1480
+ db.prepare(`
1481
+ INSERT INTO plan_versions (plan_version_id, tenant_id, plan_id, version_number, is_default, metadata_json, created_at, updated_at)
1482
+ VALUES ('pro-v1', 'default', 'pro', 1, 1, '{}', ?, ?)
1483
+ `).run(now, now);
1484
+ db.prepare(`
1485
+ INSERT INTO billing_price_mappings (tenant_id, plan_version_id, billing_interval, stripe_price_id, active, created_at, updated_at)
1486
+ VALUES ('default', 'pro-v1', 'month', 'price_pro_month', 1, ?, ?)
1487
+ `).run(now, now);
1488
+ mockCustomersList.mockResolvedValueOnce({
1489
+ data: [{ id: 'cus_email_match' }]
1490
+ });
1491
+ mockSubscriptionList.mockResolvedValueOnce({
1492
+ data: [{
1493
+ id: 'sub_email_match',
1494
+ customer: 'cus_email_match',
1495
+ status: 'active',
1496
+ trial_end: null,
1497
+ metadata: { userId: 'u1', planVersionId: 'pro-v1' },
1498
+ items: {
1499
+ data: [{
1500
+ id: 'si_email_match',
1501
+ current_period_end: Math.floor(Date.now() / 1000) + 86400,
1502
+ price: { id: 'price_pro_month', recurring: { interval: 'month' } }
1503
+ }]
1504
+ }
1505
+ }]
1506
+ });
1507
+ const statusRoute = routeHandlers.get('GET /api/taskforce/billing/status');
1508
+ const req = createMockReq('', { 'x-taskforce-runtime-mode': 'cloud' });
1509
+ const res = createMockRes();
1510
+ await statusRoute(req, res, {});
1511
+ expect(res.statusCode).toBe(200);
1512
+ const body = JSON.parse(res.body);
1513
+ expect(body.planId).toBe('pro');
1514
+ expect(body.planVersionId).toBe('pro-v1');
1515
+ expect(body.stripeCustomerId).toBe('cus_email_match');
1516
+ expect(body.stripeSubscriptionId).toBe('sub_email_match');
1517
+ });
1518
+ it('prefers subscription metadata over shared price fallback during webhook processing', async () => {
1519
+ const now = new Date().toISOString();
1520
+ db.prepare(`
1521
+ INSERT INTO plan_versions (plan_version_id, tenant_id, plan_id, version_number, is_default, metadata_json, created_at, updated_at)
1522
+ VALUES ('starter-v1', 'default', 'starter', 1, 1, '{}', ?, ?)
1523
+ `).run(now, now);
1524
+ db.prepare(`
1525
+ INSERT INTO plan_versions (plan_version_id, tenant_id, plan_id, version_number, is_default, metadata_json, created_at, updated_at)
1526
+ VALUES ('team-v1', 'default', 'team', 1, 1, '{}', ?, ?)
1527
+ `).run(now, now);
1528
+ db.prepare(`
1529
+ 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)
1530
+ VALUES ('default', 'starter-v1', 'month', 'price_shared', 'stripe', 1, 1900, 'usd', ?, ?)
1531
+ `).run(now, now);
1532
+ db.prepare(`
1533
+ 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)
1534
+ VALUES ('default', 'team-v1', 'month', 'price_shared', 'stripe', 1, 1900, 'usd', ?, ?)
1535
+ `).run(now, now);
1536
+ db.prepare(`
1537
+ INSERT INTO user_billing (user_id, tenant_id, stripe_customer_id, entitlement_state, created_at, updated_at)
1538
+ VALUES ('u1', 'default', 'cus_123', 'active', ?, ?)
1539
+ `).run(now, now);
1540
+ mockConstructEvent.mockReturnValue({
1541
+ id: 'evt_shared_metadata',
1542
+ type: 'customer.subscription.updated',
1543
+ data: {
1544
+ object: {
1545
+ id: 'sub_123',
1546
+ customer: 'cus_123',
1547
+ status: 'active',
1548
+ trial_end: null,
1549
+ metadata: { planVersionId: 'team-v1' },
1550
+ items: {
1551
+ data: [{
1552
+ current_period_end: Math.floor(Date.now() / 1000) + 86400,
1553
+ price: { id: 'price_shared', recurring: { interval: 'month' } }
1554
+ }]
1555
+ }
1556
+ }
1557
+ }
1558
+ });
1559
+ const req = createMockReq('{"id":"evt_shared_metadata"}', {
1560
+ 'x-taskforce-runtime-mode': 'cloud',
1561
+ 'stripe-signature': 'sig'
1562
+ });
1563
+ const res = createMockRes();
1564
+ await webhookHandler(req, res, {});
1565
+ expect(res.statusCode).toBe(200);
1566
+ const entitlement = db.prepare(`
1567
+ SELECT plan_version_id
1568
+ FROM account_entitlements
1569
+ WHERE account_id = 'u1' AND tenant_id = 'default'
1570
+ LIMIT 1
1571
+ `).get();
1572
+ expect(entitlement.plan_version_id).toBe('team-v1');
1573
+ });
1574
+ it('logs webhook failure when shared price fallback is ambiguous and metadata is missing', async () => {
1575
+ const now = new Date().toISOString();
1576
+ db.prepare(`
1577
+ INSERT INTO plan_versions (plan_version_id, tenant_id, plan_id, version_number, is_default, metadata_json, created_at, updated_at)
1578
+ VALUES ('starter-v1', 'default', 'starter', 1, 1, '{}', ?, ?)
1579
+ `).run(now, now);
1580
+ db.prepare(`
1581
+ INSERT INTO plan_versions (plan_version_id, tenant_id, plan_id, version_number, is_default, metadata_json, created_at, updated_at)
1582
+ VALUES ('team-v1', 'default', 'team', 1, 1, '{}', ?, ?)
1583
+ `).run(now, now);
1584
+ db.prepare(`
1585
+ 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)
1586
+ VALUES ('default', 'starter-v1', 'month', 'price_shared', 'stripe', 1, 1900, 'usd', ?, ?)
1587
+ `).run(now, now);
1588
+ db.prepare(`
1589
+ 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)
1590
+ VALUES ('default', 'team-v1', 'month', 'price_shared', 'stripe', 1, 1900, 'usd', ?, ?)
1591
+ `).run(now, now);
1592
+ db.prepare(`
1593
+ INSERT INTO user_billing (user_id, tenant_id, stripe_customer_id, entitlement_state, created_at, updated_at)
1594
+ VALUES ('u1', 'default', 'cus_456', 'active', ?, ?)
1595
+ `).run(now, now);
1596
+ mockConstructEvent.mockReturnValue({
1597
+ id: 'evt_shared_ambiguous',
1598
+ type: 'customer.subscription.updated',
1599
+ data: {
1600
+ object: {
1601
+ id: 'sub_456',
1602
+ customer: 'cus_456',
1603
+ status: 'active',
1604
+ trial_end: null,
1605
+ metadata: {},
1606
+ items: {
1607
+ data: [{
1608
+ current_period_end: Math.floor(Date.now() / 1000) + 86400,
1609
+ price: { id: 'price_shared', recurring: { interval: 'month' } }
1610
+ }]
1611
+ }
1612
+ }
1613
+ }
1614
+ });
1615
+ const req = createMockReq('{"id":"evt_shared_ambiguous"}', {
1616
+ 'x-taskforce-runtime-mode': 'cloud',
1617
+ 'stripe-signature': 'sig'
1618
+ });
1619
+ const res = createMockRes();
1620
+ await webhookHandler(req, res, {});
1621
+ expect(res.statusCode).toBe(200);
1622
+ const failure = db.prepare(`
1623
+ SELECT reason
1624
+ FROM stripe_webhook_failures
1625
+ WHERE event_id = 'evt_shared_ambiguous'
1626
+ ORDER BY created_at DESC
1627
+ LIMIT 1
1628
+ `).get();
1629
+ expect(failure.reason).toContain('Ambiguous price mapping');
1630
+ });
1631
+ });