@taskforcehq/taskforce 0.3.303 → 0.3.305

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (237) hide show
  1. package/README.md +140 -247
  2. package/dist/Taskforce.module.css +102 -5
  3. package/dist/TaskforceCore.js +635 -238
  4. package/dist/TaskforceCore.test.js +2243 -495
  5. package/dist/annotatedAttachments/service.d.ts +1 -1
  6. package/dist/annotatedAttachments/types.d.ts +1 -1
  7. package/dist/compat/workspaceSyncCompat.js +6 -0
  8. package/dist/components/features/AgentsModule.js +1 -1
  9. package/dist/components/features/AgentsModule.test.js +1 -1
  10. package/dist/components/features/DocumentIndex.d.ts +1 -1
  11. package/dist/components/features/DocumentIndex.js +1 -1
  12. package/dist/components/features/DocumentViewer.d.ts +1 -1
  13. package/dist/components/features/DocumentViewer.js +32 -9
  14. package/dist/components/features/DocumentWorkspace.d.ts +5 -22
  15. package/dist/components/features/DocumentWorkspace.js +12 -155
  16. package/dist/components/features/DocumentWorkspace.test.js +193 -6
  17. package/dist/components/features/TaskSettings.js +87 -41
  18. package/dist/components/features/TaskSettings.test.js +180 -40
  19. package/dist/components/features/documentWorkspaceModel.d.ts +24 -0
  20. package/dist/components/features/documentWorkspaceModel.js +57 -0
  21. package/dist/components/features/useDocumentWorkspaceController.d.ts +34 -0
  22. package/dist/components/features/useDocumentWorkspaceController.js +113 -0
  23. package/dist/components/task/TaskActionHeader.d.ts +2 -1
  24. package/dist/components/task/TaskActionHeader.js +3 -3
  25. package/dist/components/task/TaskActionHeader.test.js +8 -0
  26. package/dist/components/task/TaskCard.js +16 -1
  27. package/dist/components/task/TaskCard.test.js +31 -0
  28. package/dist/components/task/TaskForm.d.ts +9 -8
  29. package/dist/components/task/TaskForm.js +374 -219
  30. package/dist/components/task/TaskForm.test.js +482 -361
  31. package/dist/components/task/TaskKanban.d.ts +1 -1
  32. package/dist/components/task/TaskKanban.js +33 -161
  33. package/dist/components/task/TaskStatusActions.d.ts +6 -1
  34. package/dist/components/task/TaskStatusActions.js +8 -3
  35. package/dist/components/task/TaskStatusActions.test.js +9 -0
  36. package/dist/components/task/taskKanbanDropRules.d.ts +58 -0
  37. package/dist/components/task/taskKanbanDropRules.js +187 -0
  38. package/dist/components/task/taskKanbanDropRules.test.d.ts +1 -0
  39. package/dist/components/task/taskKanbanDropRules.test.js +114 -0
  40. package/dist/components/ui/Modal.d.ts +1 -1
  41. package/dist/components/ui/Modal.js +1 -0
  42. package/dist/components/views/AppShellHeader.js +5 -1
  43. package/dist/components/views/AppShellHeader.test.js +4 -0
  44. package/dist/components/views/PlanComparisonPage.d.ts +3 -2
  45. package/dist/components/views/PlanComparisonPage.js +9 -10
  46. package/dist/components/views/PlanComparisonPage.test.js +55 -41
  47. package/dist/components/views/PlansPage.d.ts +5 -1
  48. package/dist/components/views/PlansPage.js +150 -81
  49. package/dist/components/views/PlansPage.test.js +367 -41
  50. package/dist/components/views/StandaloneLayout.js +135 -54
  51. package/dist/components/views/WidgetView.js +11 -2
  52. package/dist/components/views/panels/FilterToolbar.test.js +6 -4
  53. package/dist/components/views/standalone/modals/AccountHubModal.d.ts +2 -14
  54. package/dist/components/views/standalone/modals/AccountHubModal.js +12 -14
  55. package/dist/components/views/standalone/modals/AccountHubModal.test.js +24 -1
  56. package/dist/components/views/standalone/modals/SyncStatusModal.js +1 -1
  57. package/dist/config/envSchema.js +1 -1
  58. package/dist/core/AiProfileService.d.ts +6 -1
  59. package/dist/core/AiProfileService.js +161 -16
  60. package/dist/core/AiProfileService.test.js +3 -1
  61. package/dist/core/AiProfiles.test.js +200 -0
  62. package/dist/core/AuthTokenService.test.d.ts +1 -0
  63. package/dist/core/AuthTokenService.test.js +78 -0
  64. package/dist/core/DeletedTaskLifecycleService.d.ts +2 -1
  65. package/dist/core/EntitlementsPolicy.test.js +75 -13
  66. package/dist/core/PlanEntitlementService.d.ts +9 -1
  67. package/dist/core/PlanEntitlementService.js +144 -19
  68. package/dist/core/PlanVersionPolicy.test.js +19 -26
  69. package/dist/core/SignupMonetizationSettings.test.js +75 -15
  70. package/dist/core/SystemAdmin.test.js +212 -56
  71. package/dist/core/TaskActivityService.d.ts +4 -1
  72. package/dist/core/TaskActivityService.js +45 -0
  73. package/dist/core/TaskAuditTimeline.test.js +52 -1
  74. package/dist/core/TaskTaxonomyValidation.test.js +53 -0
  75. package/dist/core/Taskforce.d.ts +18 -11
  76. package/dist/core/Taskforce.js +93 -12
  77. package/dist/core/WorkspacePlanMode.test.js +73 -8
  78. package/dist/core/shared.d.ts +2 -0
  79. package/dist/core/shared.js +11 -0
  80. package/dist/documentReviews/service.d.ts +1 -1
  81. package/dist/documentReviews/types.d.ts +1 -1
  82. package/dist/hooks/auth/useTaskforceAuthBootstrap.d.ts +48 -0
  83. package/dist/hooks/auth/useTaskforceAuthBootstrap.js +219 -0
  84. package/dist/hooks/sync/auth.d.ts +37 -0
  85. package/dist/hooks/sync/auth.js +63 -0
  86. package/dist/hooks/sync/bootstrap.d.ts +34 -0
  87. package/dist/hooks/sync/bootstrap.js +52 -0
  88. package/dist/hooks/sync/orchestratorShared.d.ts +36 -0
  89. package/dist/hooks/sync/orchestratorShared.js +209 -0
  90. package/dist/hooks/sync/orchestratorShared.test.d.ts +1 -0
  91. package/dist/hooks/sync/orchestratorShared.test.js +49 -0
  92. package/dist/hooks/sync/realtime.d.ts +25 -0
  93. package/dist/hooks/sync/realtime.js +60 -0
  94. package/dist/hooks/sync/recovery.d.ts +69 -0
  95. package/dist/hooks/sync/recovery.js +118 -0
  96. package/dist/hooks/sync/transfers.d.ts +123 -0
  97. package/dist/hooks/sync/transfers.js +435 -0
  98. package/dist/hooks/sync/useDataVersionRefresh.d.ts +16 -0
  99. package/dist/hooks/sync/useDataVersionRefresh.js +73 -0
  100. package/dist/hooks/ui/useResourceExport.d.ts +3 -1
  101. package/dist/hooks/ui/useResourceExport.js +5 -2
  102. package/dist/hooks/ui/useWorkflowTemplates.js +11 -6
  103. package/dist/hooks/useRealtimeSync.js +2 -1
  104. package/dist/hooks/useSyncOrchestrator.aiProfiles.test.js +8 -0
  105. package/dist/hooks/useSyncOrchestrator.context-assets.test.d.ts +1 -0
  106. package/dist/hooks/useSyncOrchestrator.context-assets.test.js +228 -0
  107. package/dist/hooks/useSyncOrchestrator.d.ts +12 -8
  108. package/dist/hooks/useSyncOrchestrator.js +388 -879
  109. package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +641 -6
  110. package/dist/hooks/useTaskMutations.d.ts +2 -1
  111. package/dist/hooks/useTaskMutations.js +57 -7
  112. package/dist/hooks/useTaskMutations.test.js +176 -0
  113. package/dist/hooks/useTaskforce.d.ts +13 -4
  114. package/dist/hooks/useTaskforce.js +302 -817
  115. package/dist/hooks/useTaskforce.sync-behavior.test.js +78 -0
  116. package/dist/hooks/useWorkspaceSyncController.d.ts +4 -1
  117. package/dist/hooks/useWorkspaceSyncController.js +33 -11
  118. package/dist/hooks/useWorkspaceSyncController.test.js +1 -0
  119. package/dist/hooks/workspace/useTaskforceApiRouting.test.js +17 -0
  120. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.d.ts +166 -0
  121. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.js +434 -0
  122. package/dist/hooks/workspace/workspaceLocalState.d.ts +19 -0
  123. package/dist/hooks/workspace/workspaceLocalState.js +100 -0
  124. package/dist/hooks/workspace/workspaceLocalState.test.d.ts +1 -0
  125. package/dist/hooks/workspace/workspaceLocalState.test.js +59 -0
  126. package/dist/localization/locales/en-US.js +2 -2
  127. package/dist/mcp/adminWorkspaceRegistrar.d.ts +2 -0
  128. package/dist/mcp/adminWorkspaceRegistrar.js +154 -0
  129. package/dist/mcp/canonicalAssetHelpers.d.ts +70 -0
  130. package/dist/mcp/canonicalAssetHelpers.js +259 -0
  131. package/dist/mcp/clientIntegrations.d.ts +12 -1
  132. package/dist/mcp/clientIntegrations.js +161 -6
  133. package/dist/mcp/clientIntegrations.test.js +76 -4
  134. package/dist/mcp/collaborationRegistrar.d.ts +2 -0
  135. package/dist/mcp/collaborationRegistrar.js +71 -0
  136. package/dist/mcp/documentAssetRegistrar.d.ts +2 -0
  137. package/dist/mcp/documentAssetRegistrar.js +346 -0
  138. package/dist/mcp/documentHelpers.d.ts +100 -0
  139. package/dist/mcp/documentHelpers.js +161 -0
  140. package/dist/mcp/runtime.js +2455 -3925
  141. package/dist/mcp/runtime.test.js +240 -8
  142. package/dist/mcp/taskAttachmentHelpers.d.ts +41 -0
  143. package/dist/mcp/taskAttachmentHelpers.js +60 -0
  144. package/dist/mcp/taskAttachmentTypes.d.ts +37 -0
  145. package/dist/mcp/taskAttachmentTypes.js +1 -0
  146. package/dist/mcp/taskPlanningRegistrar.d.ts +2 -0
  147. package/dist/mcp/taskPlanningRegistrar.js +418 -0
  148. package/dist/mcp/toolCatalog.d.ts +35 -0
  149. package/dist/mcp/toolCatalog.js +3 -0
  150. package/dist/plugins/vite.js +1 -1
  151. package/dist/runtime/appGateDefinitions.d.ts +1 -1
  152. package/dist/runtime/appGateDefinitions.js +1 -1
  153. package/dist/server/index.d.ts +1 -0
  154. package/dist/server/index.js +46 -3
  155. package/dist/server/index.test.js +12 -1
  156. package/dist/server/routes/admin.d.ts +1 -5
  157. package/dist/server/routes/admin.js +151 -80
  158. package/dist/server/routes/annotatedAttachments.d.ts +1 -1
  159. package/dist/server/routes/annotatedAttachments.js +1 -1
  160. package/dist/server/routes/auth.d.ts +1 -4
  161. package/dist/server/routes/auth.js +59 -64
  162. package/dist/server/routes/authSupport.d.ts +30 -0
  163. package/dist/server/routes/authSupport.js +81 -0
  164. package/dist/server/routes/billing.d.ts +1 -1
  165. package/dist/server/routes/billing.js +122 -255
  166. package/dist/server/routes/billing.test.js +257 -86
  167. package/dist/server/routes/documentReviews.d.ts +1 -1
  168. package/dist/server/routes/documentReviews.js +1 -1
  169. package/dist/server/routes/documents.d.ts +4 -2
  170. package/dist/server/routes/documents.js +73 -7
  171. package/dist/server/routes/primitives.d.ts +11 -0
  172. package/dist/server/routes/primitives.js +73 -0
  173. package/dist/server/routes/resources.d.ts +1 -1
  174. package/dist/server/routes/resources.js +1 -1
  175. package/dist/server/routes/shared.d.ts +31 -1
  176. package/dist/server/routes/shared.js +26 -0
  177. package/dist/server/routes/sync.d.ts +1 -1
  178. package/dist/server/routes/sync.integration.test.js +20 -0
  179. package/dist/server/routes/sync.js +111 -238
  180. package/dist/server/routes/tasks.d.ts +1 -1
  181. package/dist/server/routes/tasks.js +50 -8
  182. package/dist/server/routes/workspaces.d.ts +1 -1
  183. package/dist/server/routes/workspaces.js +1 -1
  184. package/dist/server/routes.d.ts +2 -9
  185. package/dist/server/routes.js +84 -181
  186. package/dist/server/routes.test.js +912 -32
  187. package/dist/shared/taskActor.d.ts +10 -0
  188. package/dist/shared/taskActor.js +1 -0
  189. package/dist/sync/collaborationSyncPayload.js +4 -0
  190. package/dist/sync/collaborationSyncPayload.test.js +8 -0
  191. package/dist/sync/collaborationSyncState.d.ts +3 -1
  192. package/dist/sync/collaborationSyncState.js +19 -0
  193. package/dist/sync/contentSyncState.js +28 -17
  194. package/dist/sync/syncApplyHandlers.d.ts +87 -1
  195. package/dist/sync/syncApplyHandlers.js +370 -8
  196. package/dist/sync/syncApplyHandlers.test.d.ts +1 -0
  197. package/dist/sync/syncApplyHandlers.test.js +379 -0
  198. package/dist/sync/syncService.d.ts +9 -1
  199. package/dist/sync/syncService.js +44 -2
  200. package/dist/sync/syncService.test.js +62 -0
  201. package/dist/sync/workspacePullFeed.d.ts +1 -0
  202. package/dist/sync/workspacePullFeed.js +66 -59
  203. package/dist/sync/workspacePullFeed.test.js +72 -4
  204. package/dist/sync/workspaceSyncModel.d.ts +25 -1
  205. package/dist/sync/workspaceSyncModel.js +77 -3
  206. package/dist/sync/workspaceSyncModel.test.js +153 -0
  207. package/dist/sync/workspaceSyncState.d.ts +2 -0
  208. package/dist/sync/workspaceSyncState.js +1 -0
  209. package/dist/types.d.ts +2 -10
  210. package/dist/ui/assets/AgentsModule-N2MnQBZk.js +1 -0
  211. package/dist/ui/assets/{AnnotatedAttachmentWorkspace-C_TmXZwA.js → AnnotatedAttachmentWorkspace-BG6P_GVW.js} +1 -1
  212. package/dist/ui/assets/DocumentWorkspace-B3nV_Gc5.css +1 -0
  213. package/dist/ui/assets/DocumentWorkspace-DRbDMwh8.js +252 -0
  214. package/dist/ui/assets/{InitiativesModule-4-Rh2K2n.js → InitiativesModule-Dhm7M8e7.js} +1 -1
  215. package/dist/ui/assets/{PlansPage-BlVC_lRq.css → PlansPage-C2dd2n1X.css} +1 -1
  216. package/dist/ui/assets/PlansPage-Cq0zXj3I.js +1 -0
  217. package/dist/ui/assets/TaskSettings-ln0aF7xc.js +9 -0
  218. package/dist/ui/assets/{WorkflowsModule-CGk9s3NJ.js → WorkflowsModule-BcS4afRn.js} +1 -1
  219. package/dist/ui/assets/index-C2-OXZV7.css +1 -0
  220. package/dist/ui/assets/index-DcSI5F86.js +6 -0
  221. package/dist/ui/assets/{vendor-icons-pWocEipT.js → vendor-icons-BSUaRwF8.js} +1 -1
  222. package/dist/ui/index.html +4 -4
  223. package/dist/utils/commercialLifecyclePresentation.d.ts +19 -0
  224. package/dist/utils/commercialLifecyclePresentation.js +196 -0
  225. package/dist/utils/taskActivity.js +4 -0
  226. package/dist/utils/taskActivity.test.js +25 -1
  227. package/package.json +10 -1
  228. package/scripts/check-cloud-mcp.mjs +83 -0
  229. package/scripts/playwright-auth.sh +9 -1
  230. package/scripts/run-billing-performance-smoke.sh +24 -0
  231. package/dist/ui/assets/AgentsModule-CpsTmrIW.js +0 -1
  232. package/dist/ui/assets/DocumentWorkspace-C3GyzrWm.js +0 -250
  233. package/dist/ui/assets/DocumentWorkspace-C_8T8oz-.css +0 -1
  234. package/dist/ui/assets/PlansPage-BP7AYOqr.js +0 -1
  235. package/dist/ui/assets/TaskSettings-BOC5F6Ag.js +0 -8
  236. package/dist/ui/assets/index-CLIMgR6g.js +0 -6
  237. package/dist/ui/assets/index-DneETxcu.css +0 -1
@@ -0,0 +1,81 @@
1
+ import { resolveEffectiveEmailConfig } from '../email.js';
2
+ import { resolveDeploymentConfig } from '../../config/deployment.js';
3
+ export function resolveBooleanSetting(value, fallback) {
4
+ if (typeof value === 'boolean')
5
+ return value;
6
+ if (typeof value === 'number')
7
+ return value !== 0;
8
+ const normalized = String(value || '').trim().toLowerCase();
9
+ if (!normalized)
10
+ return fallback;
11
+ if (['1', 'true', 'yes', 'on'].includes(normalized))
12
+ return true;
13
+ if (['0', 'false', 'no', 'off'].includes(normalized))
14
+ return false;
15
+ return fallback;
16
+ }
17
+ export function getRouteEmailConfig(core, context) {
18
+ if (context.authConfig?.runtimeMode !== 'cloud') {
19
+ return resolveEffectiveEmailConfig(process.env, { provider: 'console' });
20
+ }
21
+ const globalSettings = core.getGlobalSettings();
22
+ return resolveEffectiveEmailConfig(process.env, globalSettings.email || null);
23
+ }
24
+ export function buildAuthFlowBehavior(core) {
25
+ const globalSettings = core.getGlobalSettings();
26
+ const authSettings = globalSettings.auth || {};
27
+ const authRateLimitMaxRequestsRaw = Number(authSettings.authRateLimitMaxRequests);
28
+ const authRateLimitWindowMsRaw = Number(authSettings.authRateLimitWindowMs);
29
+ return {
30
+ allowSelfRegistration: resolveBooleanSetting(authSettings.allowSelfRegistration, true),
31
+ requireVerifiedEmail: resolveBooleanSetting(authSettings.requireVerifiedEmail, true),
32
+ defaultNewUserBetaAccess: resolveBooleanSetting(authSettings.defaultNewUserBetaAccess, false),
33
+ authRateLimitEnabled: resolveBooleanSetting(authSettings.authRateLimitEnabled, true),
34
+ authRateLimitMaxRequests: Number.isFinite(authRateLimitMaxRequestsRaw) && authRateLimitMaxRequestsRaw >= 1
35
+ ? Math.floor(authRateLimitMaxRequestsRaw)
36
+ : 10,
37
+ authRateLimitWindowMs: Number.isFinite(authRateLimitWindowMsRaw) && authRateLimitWindowMsRaw >= 1000
38
+ ? Math.floor(authRateLimitWindowMsRaw)
39
+ : 15 * 60_000,
40
+ };
41
+ }
42
+ export function buildAuthBehavior(core) {
43
+ const authBehavior = buildAuthFlowBehavior(core);
44
+ const onboardingPolicy = core.getOnboardingPolicy();
45
+ return {
46
+ ...authBehavior,
47
+ onboardingPolicyMode: onboardingPolicy.mode,
48
+ onboardingPlanVersionId: onboardingPolicy.planVersionId
49
+ };
50
+ }
51
+ export function buildSiteBehavior(core) {
52
+ const globalSettings = core.getGlobalSettings();
53
+ const siteSettings = globalSettings.site || {};
54
+ return {
55
+ showRunTaskforceLocally: resolveBooleanSetting(siteSettings.showRunTaskforceLocally, true),
56
+ pricingPageEnabled: resolveBooleanSetting(siteSettings.pricingPageEnabled, true)
57
+ };
58
+ }
59
+ export function shouldExposeDevAuthTokens() {
60
+ const explicit = String(process.env.TASKFORCE_AUTH_EXPOSE_DEV_TOKENS || '').trim().toLowerCase();
61
+ return explicit === '1' || explicit === 'true' || explicit === 'yes';
62
+ }
63
+ export function resolveRuntimeConfigBaseUrl(req) {
64
+ const host = String(req.headers['x-forwarded-host'] || req.headers.host || '').trim();
65
+ if (!host)
66
+ return '';
67
+ const proto = String(req.headers['x-forwarded-proto'] || '').trim().toLowerCase() === 'https'
68
+ ? 'https'
69
+ : (req.socket?.encrypted ? 'https' : 'http');
70
+ return `${proto}://${host}`.replace(/\/+$/, '');
71
+ }
72
+ export function resolvePublicAuthBaseUrl(req) {
73
+ const configured = String(process.env.TASKFORCE_AUTH_PUBLIC_BASE_URL || '').trim().replace(/\/+$/, '');
74
+ if (configured)
75
+ return configured;
76
+ const host = String(req.headers['x-forwarded-host'] || req.headers.host || '').trim();
77
+ const proto = String(req.headers['x-forwarded-proto'] || '').trim().toLowerCase() === 'http' ? 'http' : 'https';
78
+ if (!host)
79
+ return resolveDeploymentConfig(process.env).cloudBaseUrl || 'https://app.taskforcehq.ai';
80
+ return `${proto}://${host}`;
81
+ }
@@ -1,6 +1,6 @@
1
1
  import type { IncomingMessage } from 'http';
2
2
  import type { TaskforceCore } from '../../core/Taskforce.js';
3
- import type { RouteHandler, RouteContext } from '../routes.js';
3
+ import { type RouteHandler, type RouteContext } from './primitives.js';
4
4
  export type BillingInterval = 'month' | 'year';
5
5
  export type BillingPricingType = 'stripe' | 'free';
6
6
  export interface BillingRouteDependencies {
@@ -1,5 +1,5 @@
1
1
  import Stripe from 'stripe';
2
- import { parseJsonBody } from '../routes.js';
2
+ import { parseJsonBody } from './primitives.js';
3
3
  import { resolveTaskforceCloudEnvironmentFromHostname } from '../../config/cloudEnvironment.js';
4
4
  import { buildMockStripeHostedCheckoutUrl, completeMockStripeCheckoutSession, createMockStripeClient, isMockStripeClient, isMockStripeEnabled, readMockStripeCheckoutSession, renderMockStripeCheckoutHtml } from '../mockStripe.js';
5
5
  const BILLING_CONFIG_KEY_PLAN = 'billing.config.v1';
@@ -733,72 +733,18 @@ function shouldRecordCheckoutPendingState(entitlement) {
733
733
  || currentState === 'canceled';
734
734
  }
735
735
  function beginCheckoutPending(core, input) {
736
- const planId = resolvePlanIdForVersion(core, input.planVersionId);
737
736
  const updatedBy = input.updatedBy || 'checkout-start';
738
737
  const source = input.source || 'checkout-start';
739
- if (typeof core.transitionCanonicalCommercialState === 'function') {
740
- core.transitionCanonicalCommercialState({
741
- accountId: input.userId,
742
- planVersionId: input.planVersionId,
743
- state: 'checkout_pending',
744
- effectiveUntil: input.effectiveUntil || null,
745
- billingMirrorPlanId: planId,
746
- source,
747
- updatedBy
748
- });
749
- return;
750
- }
751
- const db = core.getDatabaseAdapter();
752
- const tenantId = core.getTenantId();
753
- const now = new Date().toISOString();
754
- db.prepare(`
755
- INSERT INTO account_entitlements (account_id, tenant_id, plan_version_id, entitlement_state, effective_at, effective_until, updated_by, created_at, updated_at)
756
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
757
- ON CONFLICT (account_id) DO UPDATE SET
758
- plan_version_id = excluded.plan_version_id,
759
- entitlement_state = excluded.entitlement_state,
760
- effective_at = excluded.effective_at,
761
- effective_until = excluded.effective_until,
762
- updated_by = excluded.updated_by,
763
- updated_at = excluded.updated_at
764
- `).run(input.userId, tenantId, input.planVersionId, 'checkout_pending', now, input.effectiveUntil || null, updatedBy, now, now);
765
- upsertBillingMirror(core, {
766
- userId: input.userId,
767
- planId,
768
- canonicalState: 'checkout_pending',
769
- effectiveUntil: input.effectiveUntil || null
738
+ core.transitionCanonicalCommercialState({
739
+ accountId: input.userId,
740
+ planVersionId: input.planVersionId,
741
+ state: 'checkout_pending',
742
+ effectiveUntil: input.effectiveUntil || null,
743
+ billingMirrorPlanId: resolvePlanIdForVersion(core, input.planVersionId),
744
+ source,
745
+ updatedBy
770
746
  });
771
747
  }
772
- function upsertCanonicalEntitlement(core, input) {
773
- const updatedBy = input.updatedBy || 'stripe-webhook';
774
- if (typeof core.transitionCanonicalCommercialState === 'function') {
775
- core.transitionCanonicalCommercialState({
776
- accountId: input.userId,
777
- planVersionId: input.planVersionId,
778
- state: input.state,
779
- effectiveAt: input.effectiveAt,
780
- effectiveUntil: input.effectiveUntil,
781
- billingMirrorPlanId: resolvePlanIdForVersion(core, input.planVersionId),
782
- source: updatedBy === 'checkout-confirm' ? 'checkout-confirm' : 'stripe-webhook',
783
- updatedBy
784
- });
785
- return;
786
- }
787
- const db = core.getDatabaseAdapter();
788
- const tenantId = core.getTenantId();
789
- const now = new Date().toISOString();
790
- db.prepare(`
791
- INSERT INTO account_entitlements (account_id, tenant_id, plan_version_id, entitlement_state, effective_at, effective_until, updated_by, created_at, updated_at)
792
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
793
- ON CONFLICT (account_id) DO UPDATE SET
794
- plan_version_id = excluded.plan_version_id,
795
- entitlement_state = excluded.entitlement_state,
796
- effective_at = excluded.effective_at,
797
- effective_until = excluded.effective_until,
798
- updated_by = excluded.updated_by,
799
- updated_at = excluded.updated_at
800
- `).run(input.userId, tenantId, input.planVersionId, input.state, input.effectiveAt || now, input.effectiveUntil || null, updatedBy, now, now);
801
- }
802
748
  function upsertBillingMirror(core, input) {
803
749
  const db = core.getDatabaseAdapter();
804
750
  const tenantId = core.getTenantId();
@@ -849,11 +795,15 @@ function applySubscriptionSnapshot(core, input) {
849
795
  ? (currentPeriodEnd && currentPeriodEnd > nowIso ? 'active' : 'canceled')
850
796
  : null;
851
797
  const mappedCanonicalState = canceledState || mapStripeStatusToCanonicalState(stripeStatus);
798
+ const existingPlanVersionId = String(existingEntitlement?.plan_version_id || '').trim();
852
799
  const preserveExistingAccess = mappedCanonicalState === 'checkout_pending'
800
+ && Boolean(planVersionId)
801
+ && existingPlanVersionId === planVersionId
853
802
  && isCommercialStateAppAccessible(existingEntitlement?.entitlement_state);
854
803
  const canonicalState = preserveExistingAccess
855
804
  ? String(existingEntitlement?.entitlement_state || 'active').trim().toLowerCase()
856
805
  : mappedCanonicalState;
806
+ const billingConfirmedSource = source === 'stripe-webhook' || source === 'checkout-confirm';
857
807
  const effectiveUntil = stripeStatus === 'canceled'
858
808
  ? currentPeriodEnd
859
809
  : (currentPeriodEnd || null);
@@ -880,13 +830,15 @@ function applySubscriptionSnapshot(core, input) {
880
830
  });
881
831
  }
882
832
  }
883
- else if (planVersionId) {
884
- upsertCanonicalEntitlement(core, {
885
- userId,
833
+ else if (planVersionId && billingConfirmedSource) {
834
+ core.transitionCanonicalCommercialState({
835
+ accountId: userId,
886
836
  planVersionId: planVersionId || existingEntitlement?.plan_version_id || core.getDefaultPlanVersionIdByMode('personal', { allowFallback: true }),
887
837
  state: canonicalState,
888
838
  effectiveAt: trialEnd || nowIso,
889
839
  effectiveUntil,
840
+ billingMirrorPlanId: resolvePlanIdForVersion(core, planVersionId),
841
+ source,
890
842
  updatedBy: source
891
843
  });
892
844
  }
@@ -1378,18 +1330,11 @@ export function registerBillingRoutes(deps) {
1378
1330
  res.end(JSON.stringify({ error: 'Forbidden: system admin role required.' }));
1379
1331
  return;
1380
1332
  }
1381
- try {
1382
- const requestUrl = parseRequestUrl(req);
1383
- const interval = parseInterval(requestUrl.searchParams.get('interval'));
1384
- const options = await listStripePriceOptions(interval);
1385
- res.writeHead(200, { 'Content-Type': 'application/json' });
1386
- res.end(JSON.stringify({ options }));
1387
- }
1388
- catch (error) {
1389
- const statusCode = resolveErrorStatusCode(error, 400);
1390
- res.writeHead(statusCode, { 'Content-Type': 'application/json' });
1391
- res.end(JSON.stringify({ error: String(error?.message || 'Unable to list Stripe prices'), code: error?.code }));
1392
- }
1333
+ res.writeHead(410, { 'Content-Type': 'application/json' });
1334
+ res.end(JSON.stringify({
1335
+ error: 'Manual Stripe price selection has been removed. Refresh prices from Stripe metadata instead.',
1336
+ code: 'BILLING_PRICE_MANUAL_MAPPING_REMOVED'
1337
+ }));
1393
1338
  });
1394
1339
  addRoute('PUT', '/api/taskforce/admin/billing/price-mappings', async (req, res) => {
1395
1340
  if (!ensureCloudBilling(req, res))
@@ -1399,121 +1344,11 @@ export function registerBillingRoutes(deps) {
1399
1344
  res.end(JSON.stringify({ error: 'Forbidden: system admin role required.' }));
1400
1345
  return;
1401
1346
  }
1402
- const body = await parseJsonBody(req);
1403
- const planVersionId = String(body?.planVersionId || '').trim();
1404
- const interval = parseInterval(body?.interval);
1405
- const pricingType = parsePricingType(body?.pricingType) || 'stripe';
1406
- const stripePriceId = String(body?.stripePriceId || '').trim();
1407
- const setActive = body?.active === false ? false : true;
1408
- if (!planVersionId || !interval) {
1409
- res.writeHead(400, { 'Content-Type': 'application/json' });
1410
- res.end(JSON.stringify({ error: 'planVersionId and interval are required.', code: 'BILLING_MAPPING_INPUT_REQUIRED' }));
1411
- return;
1412
- }
1413
- if (pricingType === 'stripe' && !stripePriceId) {
1414
- res.writeHead(400, { 'Content-Type': 'application/json' });
1415
- res.end(JSON.stringify({ error: 'stripePriceId is required for Stripe pricing.', code: 'BILLING_MAPPING_STRIPE_PRICE_REQUIRED' }));
1416
- return;
1417
- }
1418
- const db = core.getDatabaseAdapter();
1419
- const tenantId = core.getTenantId();
1420
- const versionExists = db.prepare(`
1421
- SELECT plan_version_id
1422
- FROM plan_versions
1423
- WHERE tenant_id = ? AND plan_version_id = ?
1424
- LIMIT 1
1425
- `).get(tenantId, planVersionId);
1426
- if (!versionExists?.plan_version_id) {
1427
- res.writeHead(404, { 'Content-Type': 'application/json' });
1428
- res.end(JSON.stringify({ error: 'Plan version not found.', code: 'PLAN_VERSION_NOT_FOUND' }));
1429
- return;
1430
- }
1431
- try {
1432
- const now = new Date().toISOString();
1433
- const targetStripePriceId = pricingType === 'free'
1434
- ? buildFreePricingSyntheticId(planVersionId, interval)
1435
- : stripePriceId;
1436
- let unitAmount = 0;
1437
- let currency = null;
1438
- if (pricingType === 'stripe') {
1439
- const stripe = getStripeClient();
1440
- const stripePrice = await stripe.prices.retrieve(stripePriceId);
1441
- const stripeInterval = parseInterval(stripePrice.recurring?.interval || '');
1442
- if (!stripePrice.active) {
1443
- res.writeHead(422, { 'Content-Type': 'application/json' });
1444
- res.end(JSON.stringify({ error: 'Stripe price is not active.', code: 'STRIPE_PRICE_INACTIVE' }));
1445
- return;
1446
- }
1447
- if (!stripeInterval) {
1448
- res.writeHead(422, { 'Content-Type': 'application/json' });
1449
- res.end(JSON.stringify({ error: 'Stripe price must be recurring month/year.', code: 'STRIPE_PRICE_INTERVAL_UNSUPPORTED' }));
1450
- return;
1451
- }
1452
- if (stripeInterval !== interval) {
1453
- res.writeHead(422, { 'Content-Type': 'application/json' });
1454
- res.end(JSON.stringify({ error: `Stripe price interval (${stripeInterval}) does not match requested interval (${interval}).`, code: 'STRIPE_PRICE_INTERVAL_MISMATCH' }));
1455
- return;
1456
- }
1457
- unitAmount = typeof stripePrice.unit_amount === 'number' ? stripePrice.unit_amount : null;
1458
- currency = stripePrice.currency || null;
1459
- }
1460
- const beforeRows = db.prepare(`
1461
- SELECT plan_version_id, billing_interval, stripe_price_id, pricing_type, active, unit_amount, currency
1462
- FROM billing_price_mappings
1463
- WHERE tenant_id = ? AND plan_version_id = ? AND billing_interval = ?
1464
- ORDER BY stripe_price_id ASC
1465
- `).all(tenantId, planVersionId, interval);
1466
- if (setActive) {
1467
- db.prepare(`
1468
- UPDATE billing_price_mappings
1469
- SET active = 0, updated_at = ?
1470
- WHERE tenant_id = ?
1471
- AND plan_version_id = ?
1472
- AND billing_interval = ?
1473
- AND stripe_price_id <> ?
1474
- AND active = 1
1475
- `).run(now, tenantId, planVersionId, interval, targetStripePriceId);
1476
- }
1477
- db.prepare(`
1478
- INSERT INTO billing_price_mappings (
1479
- tenant_id, plan_version_id, billing_interval, stripe_price_id, pricing_type, active, unit_amount, currency, created_at, updated_at
1480
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1481
- ON CONFLICT (tenant_id, plan_version_id, billing_interval, stripe_price_id) DO UPDATE SET
1482
- pricing_type = excluded.pricing_type,
1483
- active = excluded.active,
1484
- unit_amount = excluded.unit_amount,
1485
- currency = excluded.currency,
1486
- updated_at = excluded.updated_at
1487
- `).run(tenantId, planVersionId, interval, targetStripePriceId, pricingType, setActive ? 1 : 0, unitAmount, currency, now, now);
1488
- const mapping = db.prepare(`
1489
- SELECT plan_version_id, billing_interval, stripe_price_id, pricing_type, active, unit_amount, currency, created_at, updated_at
1490
- FROM billing_price_mappings
1491
- WHERE tenant_id = ? AND plan_version_id = ? AND billing_interval = ? AND stripe_price_id = ?
1492
- LIMIT 1
1493
- `).get(tenantId, planVersionId, interval, targetStripePriceId);
1494
- const afterRows = db.prepare(`
1495
- SELECT plan_version_id, billing_interval, stripe_price_id, pricing_type, active, unit_amount, currency
1496
- FROM billing_price_mappings
1497
- WHERE tenant_id = ? AND plan_version_id = ? AND billing_interval = ?
1498
- ORDER BY stripe_price_id ASC
1499
- `).all(tenantId, planVersionId, interval);
1500
- auditAdminAction(req, 'billing-price-mapping-upsert', {
1501
- planVersionId,
1502
- interval,
1503
- pricingType,
1504
- stripePriceId: pricingType === 'free' ? null : stripePriceId,
1505
- active: setActive,
1506
- before: beforeRows,
1507
- after: afterRows
1508
- });
1509
- res.writeHead(200, { 'Content-Type': 'application/json' });
1510
- res.end(JSON.stringify({ mapping }));
1511
- }
1512
- catch (error) {
1513
- const statusCode = resolveErrorStatusCode(error, 400);
1514
- res.writeHead(statusCode, { 'Content-Type': 'application/json' });
1515
- res.end(JSON.stringify({ error: String(error?.message || 'Unable to upsert price mapping'), code: error?.code }));
1516
- }
1347
+ res.writeHead(410, { 'Content-Type': 'application/json' });
1348
+ res.end(JSON.stringify({
1349
+ error: 'Manual billing price mappings have been removed. Import Stripe prices instead.',
1350
+ code: 'BILLING_PRICE_MANUAL_MAPPING_REMOVED'
1351
+ }));
1517
1352
  });
1518
1353
  addRoute('POST', '/api/taskforce/admin/billing/price-mappings/deactivate', async (req, res) => {
1519
1354
  if (!ensureCloudBilling(req, res))
@@ -1523,51 +1358,11 @@ export function registerBillingRoutes(deps) {
1523
1358
  res.end(JSON.stringify({ error: 'Forbidden: system admin role required.' }));
1524
1359
  return;
1525
1360
  }
1526
- const body = await parseJsonBody(req);
1527
- const planVersionId = String(body?.planVersionId || '').trim();
1528
- const interval = parseInterval(body?.interval);
1529
- const stripePriceId = String(body?.stripePriceId || '').trim();
1530
- if (!planVersionId || !interval) {
1531
- res.writeHead(400, { 'Content-Type': 'application/json' });
1532
- res.end(JSON.stringify({ error: 'planVersionId and interval are required.', code: 'BILLING_MAPPING_TARGET_REQUIRED' }));
1533
- return;
1534
- }
1535
- const db = core.getDatabaseAdapter();
1536
- const tenantId = core.getTenantId();
1537
- const now = new Date().toISOString();
1538
- const beforeRows = db.prepare(`
1539
- SELECT plan_version_id, billing_interval, stripe_price_id, pricing_type, active, unit_amount, currency
1540
- FROM billing_price_mappings
1541
- WHERE tenant_id = ? AND plan_version_id = ? AND billing_interval = ?
1542
- ORDER BY stripe_price_id ASC
1543
- `).all(tenantId, planVersionId, interval);
1544
- const result = stripePriceId
1545
- ? db.prepare(`
1546
- UPDATE billing_price_mappings
1547
- SET active = 0, updated_at = ?
1548
- WHERE tenant_id = ? AND plan_version_id = ? AND billing_interval = ? AND stripe_price_id = ?
1549
- `).run(now, tenantId, planVersionId, interval, stripePriceId)
1550
- : db.prepare(`
1551
- UPDATE billing_price_mappings
1552
- SET active = 0, updated_at = ?
1553
- WHERE tenant_id = ? AND plan_version_id = ? AND billing_interval = ?
1554
- `).run(now, tenantId, planVersionId, interval);
1555
- const afterRows = db.prepare(`
1556
- SELECT plan_version_id, billing_interval, stripe_price_id, pricing_type, active, unit_amount, currency
1557
- FROM billing_price_mappings
1558
- WHERE tenant_id = ? AND plan_version_id = ? AND billing_interval = ?
1559
- ORDER BY stripe_price_id ASC
1560
- `).all(tenantId, planVersionId, interval);
1561
- auditAdminAction(req, 'billing-price-mapping-deactivate', {
1562
- planVersionId,
1563
- interval,
1564
- stripePriceId: stripePriceId || null,
1565
- changedRows: Number(result?.changes || 0),
1566
- before: beforeRows,
1567
- after: afterRows
1568
- });
1569
- res.writeHead(200, { 'Content-Type': 'application/json' });
1570
- res.end(JSON.stringify({ success: true, changedRows: Number(result?.changes || 0) }));
1361
+ res.writeHead(410, { 'Content-Type': 'application/json' });
1362
+ res.end(JSON.stringify({
1363
+ error: 'Manual billing price mappings have been removed. Refresh Stripe prices instead.',
1364
+ code: 'BILLING_PRICE_MANUAL_MAPPING_REMOVED'
1365
+ }));
1571
1366
  });
1572
1367
  addRoute('GET', '/api/taskforce/admin/billing/webhook-failures', async (req, res) => {
1573
1368
  if (!ensureCloudBilling(req, res))
@@ -1821,8 +1616,8 @@ export function registerBillingRoutes(deps) {
1821
1616
  const stripe = getStripeClient();
1822
1617
  let currentSubscriptionId = String(currentBilling?.stripe_subscription_id || '').trim();
1823
1618
  const currentCustomerId = String(currentBilling?.stripe_customer_id || '').trim();
1824
- const currentPlanVersionId = String(currentEntitlement?.plan_version_id || '').trim();
1825
- const currentInterval = parseInterval(String(currentBilling?.billing_interval || '').trim());
1619
+ let currentPlanVersionId = String(currentEntitlement?.plan_version_id || '').trim();
1620
+ let currentInterval = parseInterval(String(currentBilling?.billing_interval || '').trim());
1826
1621
  const stripeAccountDiscovery = await discoverStripeAccountStateForUser(core, {
1827
1622
  userId,
1828
1623
  normalizedEmail: normalizedUserEmail,
@@ -1838,8 +1633,13 @@ export function registerBillingRoutes(deps) {
1838
1633
  return;
1839
1634
  }
1840
1635
  let resolvedCustomerId = currentCustomerId || String(stripeAccountDiscovery.preferredCustomerId || '').trim();
1841
- if (!currentSubscriptionId && stripeAccountDiscovery.activeSubscriptions.length === 1) {
1842
- const activeSubscription = stripeAccountDiscovery.activeSubscriptions[0];
1636
+ const activeSubscription = stripeAccountDiscovery.activeSubscriptions.length === 1
1637
+ ? stripeAccountDiscovery.activeSubscriptions[0]
1638
+ : null;
1639
+ let discoveredPlanVersionId = null;
1640
+ let discoveredInterval = null;
1641
+ let discoveredStripePriceId = null;
1642
+ if (!currentSubscriptionId && activeSubscription) {
1843
1643
  currentSubscriptionId = String(activeSubscription.subscription?.id || '').trim();
1844
1644
  resolvedCustomerId = String(activeSubscription.customerId || '').trim() || resolvedCustomerId;
1845
1645
  if (currentSubscriptionId || resolvedCustomerId) {
@@ -1856,7 +1656,28 @@ export function registerBillingRoutes(deps) {
1856
1656
  stripeCustomerId: resolvedCustomerId
1857
1657
  });
1858
1658
  }
1859
- if (currentSubscriptionId && currentPlanVersionId === planVersionId && currentInterval === interval) {
1659
+ if (activeSubscription) {
1660
+ const activeSubscriptionPriceId = String(activeSubscription.subscription?.items?.data?.[0]?.price?.id || '').trim();
1661
+ discoveredStripePriceId = activeSubscriptionPriceId || null;
1662
+ const activePlanVersionId = resolvePlanVersionIdForBillingEvent(core, [
1663
+ activeSubscription.subscription?.metadata?.planVersionId,
1664
+ activeSubscription.subscription?.metadata?.plan_version_id
1665
+ ], activeSubscriptionPriceId).planVersionId;
1666
+ discoveredPlanVersionId = activePlanVersionId;
1667
+ discoveredInterval = parseInterval(activeSubscription.subscription?.items?.data?.[0]?.price?.recurring?.interval || '');
1668
+ if (!currentPlanVersionId && activePlanVersionId) {
1669
+ currentPlanVersionId = activePlanVersionId;
1670
+ }
1671
+ if (!currentInterval) {
1672
+ currentInterval = discoveredInterval;
1673
+ }
1674
+ }
1675
+ const isCurrentRequestedPlan = (currentPlanVersionId === planVersionId && currentInterval === interval) || (Boolean(currentSubscriptionId)
1676
+ && discoveredPlanVersionId === planVersionId
1677
+ && discoveredInterval === interval) || (Boolean(currentSubscriptionId)
1678
+ && discoveredStripePriceId === priceId
1679
+ && discoveredInterval === interval);
1680
+ if (currentSubscriptionId && isCurrentRequestedPlan) {
1860
1681
  res.writeHead(200, { 'Content-Type': 'application/json' });
1861
1682
  res.end(JSON.stringify({
1862
1683
  checkoutState: 'already_current_plan',
@@ -1868,6 +1689,17 @@ export function registerBillingRoutes(deps) {
1868
1689
  if (currentSubscriptionId) {
1869
1690
  const current = await stripe.subscriptions.retrieve(currentSubscriptionId);
1870
1691
  const itemId = current.items.data[0]?.id;
1692
+ const currentStripePriceId = String(current.items.data[0]?.price?.id || '').trim();
1693
+ const currentSubscriptionInterval = parseInterval(current.items.data[0]?.price?.recurring?.interval || '');
1694
+ if (currentStripePriceId === priceId && currentSubscriptionInterval === interval) {
1695
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1696
+ res.end(JSON.stringify({
1697
+ checkoutState: 'already_current_plan',
1698
+ planVersionId,
1699
+ message: 'You are already on this plan.'
1700
+ }));
1701
+ return;
1702
+ }
1871
1703
  if (!itemId) {
1872
1704
  res.writeHead(400, { 'Content-Type': 'application/json' });
1873
1705
  res.end(JSON.stringify({ error: 'Subscription has no updatable line item.' }));
@@ -1892,7 +1724,7 @@ export function registerBillingRoutes(deps) {
1892
1724
  res.end(JSON.stringify({
1893
1725
  checkoutState: 'updated',
1894
1726
  planVersionId,
1895
- message: 'Your subscription was updated successfully.'
1727
+ message: 'Your subscription update was submitted. Changes will appear after Stripe confirms the update.'
1896
1728
  }));
1897
1729
  return;
1898
1730
  }
@@ -2062,20 +1894,52 @@ export function registerBillingRoutes(deps) {
2062
1894
  return;
2063
1895
  }
2064
1896
  }
1897
+ if (!subscriptionId || !subscription) {
1898
+ res.writeHead(409, { 'Content-Type': 'application/json' });
1899
+ res.end(JSON.stringify({
1900
+ error: 'Checkout session is still finalizing your subscription.',
1901
+ code: 'CHECKOUT_SESSION_INCOMPLETE'
1902
+ }));
1903
+ return;
1904
+ }
2065
1905
  core.createUserBillingRecord(resolvedUserId);
2066
1906
  upsertBillingMirror(core, {
2067
1907
  userId: resolvedUserId,
2068
1908
  stripeCustomerId: customerId || null,
2069
1909
  stripeSubscriptionId: subscriptionId || null
2070
1910
  });
2071
- if (subscription) {
2072
- resolvedPlanVersionId = applySubscriptionSnapshot(core, {
2073
- userId: resolvedUserId,
2074
- subscription,
2075
- source: 'checkout-confirm'
2076
- }).planVersionId;
2077
- updateCheckoutAttemptStatus(core, sessionId, 'confirmed');
1911
+ const snapshot = applySubscriptionSnapshot(core, {
1912
+ userId: resolvedUserId,
1913
+ subscription,
1914
+ source: 'checkout-confirm'
1915
+ });
1916
+ resolvedPlanVersionId = snapshot.planVersionId;
1917
+ if (!resolvedPlanVersionId) {
1918
+ res.writeHead(409, { 'Content-Type': 'application/json' });
1919
+ res.end(JSON.stringify({
1920
+ error: 'Checkout completed, but the purchased plan could not be resolved yet.',
1921
+ code: 'CHECKOUT_PLAN_VERSION_UNRESOLVED'
1922
+ }));
1923
+ return;
2078
1924
  }
1925
+ const updatedEntitlement = getEntitlementRow(core, resolvedUserId);
1926
+ const updatedPlanVersionId = String(updatedEntitlement?.plan_version_id || '').trim();
1927
+ const updatedState = String(updatedEntitlement?.entitlement_state || '').trim().toLowerCase();
1928
+ const canonicalSettled = snapshot.canonicalState !== 'checkout_pending'
1929
+ && updatedPlanVersionId === resolvedPlanVersionId
1930
+ && updatedState === snapshot.canonicalState;
1931
+ if (!canonicalSettled) {
1932
+ console.warn(`[Taskforce Billing] Checkout confirmation still settling for user=${resolvedUserId} session=${sessionId} subscription=${subscriptionId || '(missing)'} `
1933
+ + `planVersionId=${resolvedPlanVersionId} canonicalState=${snapshot.canonicalState} persistedPlanVersionId=${updatedPlanVersionId || '(missing)'} `
1934
+ + `persistedState=${updatedState || '(missing)'}`);
1935
+ res.writeHead(409, { 'Content-Type': 'application/json' });
1936
+ res.end(JSON.stringify({
1937
+ error: 'Checkout session is still finalizing your plan.',
1938
+ code: 'CHECKOUT_SESSION_INCOMPLETE'
1939
+ }));
1940
+ return;
1941
+ }
1942
+ updateCheckoutAttemptStatus(core, sessionId, 'confirmed');
2079
1943
  res.writeHead(200, { 'Content-Type': 'application/json' });
2080
1944
  res.end(JSON.stringify({
2081
1945
  success: true,
@@ -2140,7 +2004,7 @@ export function registerBillingRoutes(deps) {
2140
2004
  const baseUrl = resolveBaseUrl(req);
2141
2005
  const session = await stripe.billingPortal.sessions.create({
2142
2006
  customer: customerId,
2143
- return_url: `${baseUrl}/settings`
2007
+ return_url: `${baseUrl}/?screen=plans`
2144
2008
  });
2145
2009
  res.writeHead(200, { 'Content-Type': 'application/json' });
2146
2010
  res.end(JSON.stringify({ url: session.url, returnBaseUrl: baseUrl }));
@@ -2385,12 +2249,15 @@ export function registerBillingRoutes(deps) {
2385
2249
  });
2386
2250
  return;
2387
2251
  }
2388
- upsertCanonicalEntitlement(core, {
2389
- userId,
2252
+ core.transitionCanonicalCommercialState({
2253
+ accountId: userId,
2390
2254
  planVersionId: planVersionId || existingEntitlement?.plan_version_id || core.getDefaultPlanVersionIdByMode('personal', { allowFallback: true }),
2391
2255
  state: canonicalState,
2392
2256
  effectiveAt: new Date().toISOString(),
2393
- effectiveUntil: currentPeriodEnd
2257
+ effectiveUntil: currentPeriodEnd,
2258
+ billingMirrorPlanId: resolvePlanIdForVersion(core, planVersionId),
2259
+ source: 'stripe-webhook',
2260
+ updatedBy: 'stripe-webhook'
2394
2261
  });
2395
2262
  markLatestOpenCheckoutAttemptConfirmed(core, {
2396
2263
  userId,