@taskforcehq/taskforce 0.3.302 → 0.3.304

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 (192) hide show
  1. package/dist/Taskforce.module.css +113 -5
  2. package/dist/TaskforceCore.js +560 -208
  3. package/dist/TaskforceCore.test.js +1833 -91
  4. package/dist/annotatedAttachments/service.d.ts +1 -1
  5. package/dist/annotatedAttachments/types.d.ts +1 -1
  6. package/dist/components/features/AgentsModule.js +1 -1
  7. package/dist/components/features/AgentsModule.test.js +1 -1
  8. package/dist/components/features/TaskSettings.js +58 -38
  9. package/dist/components/features/TaskSettings.test.js +139 -40
  10. package/dist/components/task/TaskActionHeader.d.ts +2 -1
  11. package/dist/components/task/TaskActionHeader.js +3 -3
  12. package/dist/components/task/TaskActionHeader.test.js +8 -0
  13. package/dist/components/task/TaskCard.js +16 -1
  14. package/dist/components/task/TaskCard.test.js +31 -0
  15. package/dist/components/task/TaskForm.d.ts +2 -1
  16. package/dist/components/task/TaskForm.js +22 -4
  17. package/dist/components/task/TaskForm.test.js +46 -0
  18. package/dist/components/task/TaskKanban.d.ts +1 -1
  19. package/dist/components/task/TaskKanban.js +31 -160
  20. package/dist/components/task/TaskKanban.test.js +17 -0
  21. package/dist/components/task/TaskStatusActions.d.ts +6 -1
  22. package/dist/components/task/TaskStatusActions.js +8 -3
  23. package/dist/components/task/TaskStatusActions.test.js +9 -0
  24. package/dist/components/task/taskKanbanDropRules.d.ts +58 -0
  25. package/dist/components/task/taskKanbanDropRules.js +187 -0
  26. package/dist/components/task/taskKanbanDropRules.test.d.ts +1 -0
  27. package/dist/components/task/taskKanbanDropRules.test.js +114 -0
  28. package/dist/components/ui/Modal.d.ts +1 -1
  29. package/dist/components/ui/Modal.js +1 -0
  30. package/dist/components/views/AppShellHeader.js +5 -1
  31. package/dist/components/views/AppShellHeader.test.js +4 -0
  32. package/dist/components/views/PlanComparisonPage.d.ts +3 -2
  33. package/dist/components/views/PlanComparisonPage.js +4 -2
  34. package/dist/components/views/PlanComparisonPage.test.js +4 -0
  35. package/dist/components/views/PlansPage.d.ts +7 -1
  36. package/dist/components/views/PlansPage.js +139 -53
  37. package/dist/components/views/PlansPage.test.d.ts +1 -0
  38. package/dist/components/views/PlansPage.test.js +157 -0
  39. package/dist/components/views/StandaloneLayout.d.ts +1 -0
  40. package/dist/components/views/StandaloneLayout.js +216 -186
  41. package/dist/components/views/WidgetView.js +11 -2
  42. package/dist/components/views/standalone/modals/AccountHubModal.d.ts +8 -3
  43. package/dist/components/views/standalone/modals/AccountHubModal.js +16 -2
  44. package/dist/components/views/standalone/modals/AccountHubModal.test.d.ts +1 -0
  45. package/dist/components/views/standalone/modals/AccountHubModal.test.js +26 -0
  46. package/dist/components/views/standalone/modals/SyncStatusModal.js +1 -1
  47. package/dist/core/DeletedTaskLifecycleService.d.ts +2 -1
  48. package/dist/core/EntitlementsPolicy.test.js +2 -2
  49. package/dist/core/GlobalSettingsService.js +78 -18
  50. package/dist/core/PlanEntitlementService.d.ts +31 -5
  51. package/dist/core/PlanEntitlementService.js +350 -222
  52. package/dist/core/SignupMonetizationSettings.test.js +208 -49
  53. package/dist/core/SystemAdmin.test.js +186 -96
  54. package/dist/core/TaskActivityService.d.ts +4 -1
  55. package/dist/core/TaskActivityService.js +45 -0
  56. package/dist/core/TaskAuditTimeline.test.js +52 -1
  57. package/dist/core/Taskforce.d.ts +65 -20
  58. package/dist/core/Taskforce.js +261 -51
  59. package/dist/core/Taskforce.mcpAuth.test.js +0 -2
  60. package/dist/core/UserProfileAvatarDrafts.test.js +12 -3
  61. package/dist/core/WorkspacePlanMode.test.js +65 -0
  62. package/dist/core/types.d.ts +11 -12
  63. package/dist/documentReviews/service.d.ts +1 -1
  64. package/dist/documentReviews/types.d.ts +1 -1
  65. package/dist/hooks/auth/useTaskforceAuthBootstrap.d.ts +48 -0
  66. package/dist/hooks/auth/useTaskforceAuthBootstrap.js +219 -0
  67. package/dist/hooks/sync/auth.d.ts +37 -0
  68. package/dist/hooks/sync/auth.js +61 -0
  69. package/dist/hooks/sync/bootstrap.d.ts +34 -0
  70. package/dist/hooks/sync/bootstrap.js +52 -0
  71. package/dist/hooks/sync/orchestratorShared.d.ts +36 -0
  72. package/dist/hooks/sync/orchestratorShared.js +209 -0
  73. package/dist/hooks/sync/orchestratorShared.test.d.ts +1 -0
  74. package/dist/hooks/sync/orchestratorShared.test.js +49 -0
  75. package/dist/hooks/sync/realtime.d.ts +25 -0
  76. package/dist/hooks/sync/realtime.js +60 -0
  77. package/dist/hooks/sync/recovery.d.ts +69 -0
  78. package/dist/hooks/sync/recovery.js +118 -0
  79. package/dist/hooks/sync/transfers.d.ts +122 -0
  80. package/dist/hooks/sync/transfers.js +431 -0
  81. package/dist/hooks/sync/useDataVersionRefresh.d.ts +16 -0
  82. package/dist/hooks/sync/useDataVersionRefresh.js +73 -0
  83. package/dist/hooks/ui/useResourceExport.d.ts +3 -1
  84. package/dist/hooks/ui/useResourceExport.js +5 -2
  85. package/dist/hooks/ui/useWorkflowTemplates.js +11 -6
  86. package/dist/hooks/useRealtimeSync.js +2 -1
  87. package/dist/hooks/useSyncOrchestrator.aiProfiles.test.js +8 -0
  88. package/dist/hooks/useSyncOrchestrator.context-assets.test.d.ts +1 -0
  89. package/dist/hooks/useSyncOrchestrator.context-assets.test.js +228 -0
  90. package/dist/hooks/useSyncOrchestrator.d.ts +5 -4
  91. package/dist/hooks/useSyncOrchestrator.js +315 -860
  92. package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +665 -8
  93. package/dist/hooks/useTaskMutations.js +45 -6
  94. package/dist/hooks/useTaskMutations.test.js +147 -0
  95. package/dist/hooks/useTaskforce.d.ts +11 -2
  96. package/dist/hooks/useTaskforce.js +290 -812
  97. package/dist/hooks/useWorkspaceSyncController.d.ts +1 -0
  98. package/dist/hooks/useWorkspaceSyncController.js +22 -10
  99. package/dist/hooks/workspace/useTaskforceApiRouting.test.js +17 -0
  100. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.d.ts +166 -0
  101. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.js +434 -0
  102. package/dist/hooks/workspace/workspaceLocalState.d.ts +19 -0
  103. package/dist/hooks/workspace/workspaceLocalState.js +100 -0
  104. package/dist/hooks/workspace/workspaceLocalState.test.d.ts +1 -0
  105. package/dist/hooks/workspace/workspaceLocalState.test.js +59 -0
  106. package/dist/localization/locales/en-US.js +2 -2
  107. package/dist/mcp/canonicalAssetHelpers.d.ts +70 -0
  108. package/dist/mcp/canonicalAssetHelpers.js +259 -0
  109. package/dist/mcp/clientIntegrations.d.ts +12 -1
  110. package/dist/mcp/clientIntegrations.js +161 -6
  111. package/dist/mcp/clientIntegrations.test.js +76 -4
  112. package/dist/mcp/documentHelpers.d.ts +100 -0
  113. package/dist/mcp/documentHelpers.js +161 -0
  114. package/dist/mcp/runtime.js +264 -505
  115. package/dist/mcp/runtime.test.js +353 -89
  116. package/dist/mcp/taskAttachmentHelpers.d.ts +41 -0
  117. package/dist/mcp/taskAttachmentHelpers.js +60 -0
  118. package/dist/mcp/taskAttachmentTypes.d.ts +37 -0
  119. package/dist/mcp/taskAttachmentTypes.js +1 -0
  120. package/dist/migrations/billingSchemaParity.test.js +3 -0
  121. package/dist/migrations/taskSchemaMigrations.js +15 -0
  122. package/dist/plugins/vite.js +1 -1
  123. package/dist/runtime/appGateDefinitions.d.ts +1 -1
  124. package/dist/runtime/appGateDefinitions.js +1 -1
  125. package/dist/server/index.cookieProxy.test.js +1 -0
  126. package/dist/server/index.d.ts +1 -0
  127. package/dist/server/index.js +48 -3
  128. package/dist/server/index.test.js +14 -2
  129. package/dist/server/mockStripe.d.ts +1 -0
  130. package/dist/server/mockStripe.js +2 -0
  131. package/dist/server/routes/admin.js +91 -40
  132. package/dist/server/routes/auth.d.ts +1 -0
  133. package/dist/server/routes/auth.js +231 -55
  134. package/dist/server/routes/billing.js +473 -39
  135. package/dist/server/routes/billing.test.js +972 -37
  136. package/dist/server/routes/shared.d.ts +30 -0
  137. package/dist/server/routes/shared.js +31 -3
  138. package/dist/server/routes/sync.integration.test.js +180 -0
  139. package/dist/server/routes/sync.js +125 -242
  140. package/dist/server/routes/tasks.js +49 -7
  141. package/dist/server/routes.js +11 -22
  142. package/dist/server/routes.test.js +530 -81
  143. package/dist/shared/taskActor.d.ts +10 -0
  144. package/dist/shared/taskActor.js +1 -0
  145. package/dist/sync/cloudSyncApi.d.ts +1 -0
  146. package/dist/sync/collaborationSyncPayload.js +4 -0
  147. package/dist/sync/collaborationSyncPayload.test.js +8 -0
  148. package/dist/sync/collaborationSyncState.d.ts +3 -1
  149. package/dist/sync/collaborationSyncState.js +19 -0
  150. package/dist/sync/contentSyncState.d.ts +10 -0
  151. package/dist/sync/contentSyncState.js +277 -14
  152. package/dist/sync/syncApplyHandlers.d.ts +87 -1
  153. package/dist/sync/syncApplyHandlers.js +370 -8
  154. package/dist/sync/syncApplyHandlers.test.d.ts +1 -0
  155. package/dist/sync/syncApplyHandlers.test.js +377 -0
  156. package/dist/sync/syncService.d.ts +9 -1
  157. package/dist/sync/syncService.js +57 -10
  158. package/dist/sync/syncService.test.js +48 -0
  159. package/dist/sync/workspacePullFeed.d.ts +11 -0
  160. package/dist/sync/workspacePullFeed.js +123 -68
  161. package/dist/sync/workspacePullFeed.test.js +180 -0
  162. package/dist/sync/workspaceSyncModel.d.ts +22 -1
  163. package/dist/sync/workspaceSyncModel.js +65 -0
  164. package/dist/sync/workspaceSyncModel.test.js +119 -0
  165. package/dist/types.d.ts +2 -10
  166. package/dist/ui/assets/AgentsModule-BLWVbuKP.js +1 -0
  167. package/dist/ui/assets/{AnnotatedAttachmentWorkspace-Dvi02Y7q.js → AnnotatedAttachmentWorkspace-BlS-cqnl.js} +1 -1
  168. package/dist/ui/assets/{DocumentWorkspace-iys3zfca.js → DocumentWorkspace-BH_6DF6x.js} +1 -1
  169. package/dist/ui/assets/{InitiativesModule-DJvLJI3A.js → InitiativesModule-BjR6iBf9.js} +1 -1
  170. package/dist/ui/assets/{PlansPage-BlVC_lRq.css → PlansPage-C2dd2n1X.css} +1 -1
  171. package/dist/ui/assets/PlansPage-CSDQ4sLa.js +1 -0
  172. package/dist/ui/assets/TaskSettings-CEDy34Jo.js +9 -0
  173. package/dist/ui/assets/{WorkflowsModule-BguHeQaV.js → WorkflowsModule-DnFqdUME.js} +1 -1
  174. package/dist/ui/assets/index-DXUdtH1B.js +6 -0
  175. package/dist/ui/assets/index-Ii0B0rTO.css +1 -0
  176. package/dist/ui/assets/{vendor-icons-C8EED3Oh.js → vendor-icons-I7ZwG1z_.js} +1 -1
  177. package/dist/ui/index.html +4 -4
  178. package/dist/utils/accountProfileSummaryCache.d.ts +31 -0
  179. package/dist/utils/accountProfileSummaryCache.js +97 -0
  180. package/dist/utils/accountProfileSummaryCache.test.d.ts +1 -0
  181. package/dist/utils/accountProfileSummaryCache.test.js +63 -0
  182. package/dist/utils/taskActivity.js +4 -0
  183. package/dist/utils/taskActivity.test.js +25 -1
  184. package/package.json +5 -3
  185. package/scripts/playwright-auth.sh +4 -0
  186. package/dist/ui/assets/AgentsModule-JG5jc3he.js +0 -1
  187. package/dist/ui/assets/PlansPage-BMPlpFpw.js +0 -1
  188. package/dist/ui/assets/TaskSettings-Bqd4cCzW.js +0 -8
  189. package/dist/ui/assets/index-MXeWoebC.css +0 -1
  190. package/dist/ui/assets/index-YJ4k5QNa.js +0 -6
  191. package/dist/utils/billingStatusCache.d.ts +0 -21
  192. package/dist/utils/billingStatusCache.js +0 -70
@@ -4,39 +4,77 @@ import os from 'os';
4
4
  import path from 'path';
5
5
  import { TaskforceCore } from './Taskforce';
6
6
  function makeProjectRoot() {
7
- return fs.mkdtempSync(path.join(os.tmpdir(), 'taskforce-signup-monetization-'));
7
+ return fs.mkdtempSync(path.join(os.tmpdir(), 'taskforce-onboarding-policy-'));
8
8
  }
9
- function openCore(projectRoot) {
10
- const core = new TaskforceCore({
11
- projectRoot,
12
- storagePath: path.join(projectRoot, '.taskforce'),
13
- tenantId: path.basename(projectRoot)
14
- });
9
+ function insertFreePriceMapping(core, planVersionId) {
15
10
  const db = core.db;
11
+ const tenantId = core.getTenantId();
16
12
  const now = new Date().toISOString();
17
- core.upsertPlanCatalog({ planId: 'starter', displayName: 'Starter' });
18
13
  db.prepare(`
19
14
  INSERT OR IGNORE INTO billing_price_mappings (
20
15
  tenant_id, plan_version_id, billing_interval, stripe_price_id, pricing_type, active, unit_amount, currency, created_at, updated_at
21
16
  ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
22
- `).run(path.basename(projectRoot), 'starter-v1', 'month', 'free:starter-v1:month', 'free', 1, 0, null, now, now);
17
+ `).run(tenantId, planVersionId, 'month', `free:${planVersionId}:month`, 'free', 1, 0, null, now, now);
18
+ }
19
+ function openCore(projectRoot) {
20
+ const core = new TaskforceCore({
21
+ projectRoot,
22
+ storagePath: path.join(projectRoot, '.taskforce'),
23
+ tenantId: path.basename(projectRoot)
24
+ });
23
25
  core.upsertPlanCatalog({ planId: 'starter', displayName: 'Starter', status: 'active' });
26
+ core.updatePlanVersion({
27
+ planId: 'starter',
28
+ versionNumber: 1,
29
+ seatLimit: 1
30
+ });
24
31
  core.updatePlanVersionFeatures({
25
32
  planVersionId: 'starter-v1',
26
33
  features: [
27
34
  { featureKey: 'workspace.switching', access: 'enabled' }
28
35
  ]
29
36
  });
37
+ insertFreePriceMapping(core, 'starter-v1');
30
38
  return core;
31
39
  }
32
- describe('TaskforceCore signup monetization settings', () => {
33
- it('normalizes defaults to auto_assign_default with no version id', () => {
40
+ describe('TaskforceCore onboarding policy', () => {
41
+ it('defaults to auto_grant_free when a free personal plan version is available', () => {
34
42
  const projectRoot = makeProjectRoot();
35
43
  const core = openCore(projectRoot);
36
44
  try {
37
- expect(core.getSignupMonetizationSettings()).toEqual({
38
- strategy: 'auto_assign_default',
39
- defaultSignupPlanVersionId: null
45
+ expect(core.getOnboardingPolicy()).toEqual({
46
+ mode: 'auto_grant_free',
47
+ planVersionId: 'starter-v1'
48
+ });
49
+ }
50
+ finally {
51
+ core.close();
52
+ fs.rmSync(projectRoot, { recursive: true, force: true });
53
+ }
54
+ });
55
+ it('falls back to require_plan_selection when no free onboarding plan is available', () => {
56
+ const projectRoot = makeProjectRoot();
57
+ const core = new TaskforceCore({
58
+ projectRoot,
59
+ storagePath: path.join(projectRoot, '.taskforce'),
60
+ tenantId: path.basename(projectRoot)
61
+ });
62
+ core.upsertPlanCatalog({ planId: 'starter', displayName: 'Starter', status: 'active' });
63
+ core.updatePlanVersion({
64
+ planId: 'starter',
65
+ versionNumber: 1,
66
+ seatLimit: 1
67
+ });
68
+ core.updatePlanVersionFeatures({
69
+ planVersionId: 'starter-v1',
70
+ features: [
71
+ { featureKey: 'workspace.switching', access: 'enabled' }
72
+ ]
73
+ });
74
+ try {
75
+ expect(core.getOnboardingPolicy()).toEqual({
76
+ mode: 'require_plan_selection',
77
+ planVersionId: null
40
78
  });
41
79
  }
42
80
  finally {
@@ -44,16 +82,16 @@ describe('TaskforceCore signup monetization settings', () => {
44
82
  fs.rmSync(projectRoot, { recursive: true, force: true });
45
83
  }
46
84
  });
47
- it('fails validation for auto_assign_default without a default version id', () => {
85
+ it('fails validation for auto_grant_free without a plan version id', () => {
48
86
  const projectRoot = makeProjectRoot();
49
87
  const core = openCore(projectRoot);
50
88
  try {
51
- expect(core.validateSignupMonetizationSettings({
52
- strategy: 'auto_assign_default',
53
- defaultSignupPlanVersionId: null
89
+ expect(core.validateOnboardingPolicy({
90
+ mode: 'auto_grant_free',
91
+ planVersionId: null
54
92
  })).toEqual(expect.objectContaining({
55
93
  valid: false,
56
- code: 'SIGNUP_PLAN_VERSION_REQUIRED'
94
+ code: 'ONBOARDING_PLAN_VERSION_REQUIRED'
57
95
  }));
58
96
  }
59
97
  finally {
@@ -65,12 +103,12 @@ describe('TaskforceCore signup monetization settings', () => {
65
103
  const projectRoot = makeProjectRoot();
66
104
  const core = openCore(projectRoot);
67
105
  try {
68
- expect(core.validateSignupMonetizationSettings({
69
- strategy: 'auto_assign_default',
70
- defaultSignupPlanVersionId: 'missing-v9'
106
+ expect(core.validateOnboardingPolicy({
107
+ mode: 'auto_grant_free',
108
+ planVersionId: 'missing-v9'
71
109
  })).toEqual(expect.objectContaining({
72
110
  valid: false,
73
- code: 'SIGNUP_PLAN_VERSION_NOT_FOUND'
111
+ code: 'ONBOARDING_PLAN_VERSION_NOT_FOUND'
74
112
  }));
75
113
  }
76
114
  finally {
@@ -83,12 +121,12 @@ describe('TaskforceCore signup monetization settings', () => {
83
121
  const core = openCore(projectRoot);
84
122
  try {
85
123
  core.upsertPlanCatalog({ planId: 'starter', displayName: 'Starter', status: 'archived' });
86
- expect(core.validateSignupMonetizationSettings({
87
- strategy: 'auto_assign_default',
88
- defaultSignupPlanVersionId: 'starter-v1'
124
+ expect(core.validateOnboardingPolicy({
125
+ mode: 'auto_grant_free',
126
+ planVersionId: 'starter-v1'
89
127
  })).toEqual(expect.objectContaining({
90
128
  valid: false,
91
- code: 'SIGNUP_PLAN_NOT_ENABLED'
129
+ code: 'ONBOARDING_PLAN_NOT_ENABLED'
92
130
  }));
93
131
  }
94
132
  finally {
@@ -96,13 +134,31 @@ describe('TaskforceCore signup monetization settings', () => {
96
134
  fs.rmSync(projectRoot, { recursive: true, force: true });
97
135
  }
98
136
  });
99
- it('passes validation for plan_selection_required without a default version id', () => {
137
+ it('fails validation when auto_grant_free points at a version without free pricing', () => {
100
138
  const projectRoot = makeProjectRoot();
101
139
  const core = openCore(projectRoot);
102
140
  try {
103
- expect(core.validateSignupMonetizationSettings({
104
- strategy: 'plan_selection_required',
105
- defaultSignupPlanVersionId: null
141
+ core.upsertPlanCatalog({ planId: 'collab', displayName: 'Collab', status: 'active' });
142
+ expect(core.validateOnboardingPolicy({
143
+ mode: 'auto_grant_free',
144
+ planVersionId: 'collab-v1'
145
+ })).toEqual(expect.objectContaining({
146
+ valid: false,
147
+ code: 'ONBOARDING_PLAN_MUST_BE_FREE'
148
+ }));
149
+ }
150
+ finally {
151
+ core.close();
152
+ fs.rmSync(projectRoot, { recursive: true, force: true });
153
+ }
154
+ });
155
+ it('passes validation for require_plan_selection without a plan version id', () => {
156
+ const projectRoot = makeProjectRoot();
157
+ const core = openCore(projectRoot);
158
+ try {
159
+ expect(core.validateOnboardingPolicy({
160
+ mode: 'require_plan_selection',
161
+ planVersionId: null
106
162
  })).toEqual({ valid: true });
107
163
  }
108
164
  finally {
@@ -110,19 +166,21 @@ describe('TaskforceCore signup monetization settings', () => {
110
166
  fs.rmSync(projectRoot, { recursive: true, force: true });
111
167
  }
112
168
  });
113
- it('does not backfill canonical entitlements for pending plan selection accounts', () => {
169
+ it('allows backfill transitions to create pending plan selection entitlements', () => {
114
170
  const projectRoot = makeProjectRoot();
115
171
  const tenantId = path.basename(projectRoot);
116
- const initial = openCore(projectRoot);
172
+ const core = openCore(projectRoot);
117
173
  try {
118
174
  const now = new Date().toISOString();
119
- initial.updateGlobalSettings({
120
- auth: {
121
- signupMonetizationStrategy: 'plan_selection_required',
122
- defaultSignupPlanVersionId: null
175
+ core.updateGlobalSettings({
176
+ billing: {
177
+ onboardingPolicy: {
178
+ mode: 'require_plan_selection',
179
+ planVersionId: null
180
+ }
123
181
  }
124
182
  });
125
- const db = initial.getDatabaseAdapter();
183
+ const db = core.getDatabaseAdapter();
126
184
  db.prepare(`
127
185
  INSERT INTO users (id, tenant_id, email, display_name, auth_provider, external_auth_id, is_disabled, created_at, updated_at)
128
186
  VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?)
@@ -131,28 +189,129 @@ describe('TaskforceCore signup monetization settings', () => {
131
189
  INSERT INTO user_billing (user_id, tenant_id, entitlement_state, created_at, updated_at)
132
190
  VALUES (?, ?, ?, ?, ?)
133
191
  `).run('pending-u1', tenantId, 'pending_plan_selection', now, now);
192
+ core.transitionCanonicalCommercialState({
193
+ accountId: 'pending-u1',
194
+ planVersionId: 'starter-v1',
195
+ state: 'pending_plan_selection',
196
+ source: 'backfill',
197
+ updatedBy: 'backfill',
198
+ billingMirrorPlanId: null,
199
+ effectiveAt: now,
200
+ createdAt: now,
201
+ updatedAt: now
202
+ });
203
+ expect(core.hasAccountEntitlement('pending-u1')).toBe(true);
204
+ expect(core.getAccountAccessStatus('pending-u1')).toEqual(expect.objectContaining({
205
+ gate: 'plan_selection_required',
206
+ canAccessApp: false,
207
+ canAccessPlans: true,
208
+ planSelectionRequired: true
209
+ }));
134
210
  }
135
211
  finally {
136
- initial.close();
212
+ core.close();
213
+ fs.rmSync(projectRoot, { recursive: true, force: true });
137
214
  }
138
- const reopened = openCore(projectRoot);
215
+ });
216
+ it('treats a live checkout_pending entitlement as checkout recovery state', () => {
217
+ const projectRoot = makeProjectRoot();
218
+ const tenantId = path.basename(projectRoot);
219
+ const core = openCore(projectRoot);
139
220
  try {
140
- reopened.updateGlobalSettings({
141
- auth: {
142
- signupMonetizationStrategy: 'plan_selection_required',
143
- defaultSignupPlanVersionId: null
144
- }
221
+ const now = new Date().toISOString();
222
+ const checkoutExpiresAt = new Date(Date.now() + (60 * 60 * 1000)).toISOString();
223
+ core.upsertPlanCatalog({ planId: 'pro', displayName: 'Pro', status: 'active' });
224
+ const db = core.getDatabaseAdapter();
225
+ db.prepare(`
226
+ INSERT INTO users (id, tenant_id, email, display_name, auth_provider, external_auth_id, is_disabled, created_at, updated_at)
227
+ VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?)
228
+ `).run('checkout-u1', tenantId, 'checkout@example.com', 'Checkout', 'token', 'checkout-u1', now, now);
229
+ core.transitionCanonicalCommercialState({
230
+ accountId: 'checkout-u1',
231
+ planVersionId: 'pro-v1',
232
+ state: 'checkout_pending',
233
+ source: 'checkout-start',
234
+ updatedBy: 'checkout-start',
235
+ billingMirrorPlanId: 'pro',
236
+ effectiveAt: now,
237
+ effectiveUntil: checkoutExpiresAt,
238
+ createdAt: now,
239
+ updatedAt: now
240
+ });
241
+ expect(core.getAccountAccessStatus('checkout-u1')).toEqual(expect.objectContaining({
242
+ gate: 'checkout_pending',
243
+ canAccessApp: false,
244
+ canAccessPlans: true,
245
+ planSelectionRequired: false,
246
+ message: 'Complete checkout to continue.'
247
+ }));
248
+ }
249
+ finally {
250
+ core.close();
251
+ fs.rmSync(projectRoot, { recursive: true, force: true });
252
+ }
253
+ });
254
+ it('treats an expired checkout_pending entitlement as requiring plan reselection', () => {
255
+ const projectRoot = makeProjectRoot();
256
+ const tenantId = path.basename(projectRoot);
257
+ const core = openCore(projectRoot);
258
+ try {
259
+ const now = new Date().toISOString();
260
+ const expiredAt = new Date(Date.now() - (60 * 1000)).toISOString();
261
+ core.upsertPlanCatalog({ planId: 'pro', displayName: 'Pro', status: 'active' });
262
+ const db = core.getDatabaseAdapter();
263
+ db.prepare(`
264
+ INSERT INTO users (id, tenant_id, email, display_name, auth_provider, external_auth_id, is_disabled, created_at, updated_at)
265
+ VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?)
266
+ `).run('expired-checkout-u1', tenantId, 'expired@example.com', 'Expired Checkout', 'token', 'expired-checkout-u1', now, now);
267
+ core.transitionCanonicalCommercialState({
268
+ accountId: 'expired-checkout-u1',
269
+ planVersionId: 'pro-v1',
270
+ state: 'checkout_pending',
271
+ source: 'checkout-start',
272
+ updatedBy: 'checkout-start',
273
+ billingMirrorPlanId: 'pro',
274
+ effectiveAt: now,
275
+ effectiveUntil: expiredAt,
276
+ createdAt: now,
277
+ updatedAt: now
145
278
  });
146
- expect(reopened.hasAccountEntitlement('pending-u1')).toBe(false);
147
- expect(reopened.getAccountAccessStatus('pending-u1')).toEqual(expect.objectContaining({
279
+ expect(core.getAccountAccessStatus('expired-checkout-u1')).toEqual(expect.objectContaining({
148
280
  gate: 'plan_selection_required',
149
281
  canAccessApp: false,
150
282
  canAccessPlans: true,
151
- planSelectionRequired: true
283
+ planSelectionRequired: true,
284
+ message: 'Choose a plan to continue.'
285
+ }));
286
+ expect(core.getAccountEntitlement('expired-checkout-u1')).toEqual(expect.objectContaining({
287
+ state: 'pending_plan_selection',
288
+ effectiveUntil: null
289
+ }));
290
+ const entitlementRow = db.prepare(`
291
+ SELECT entitlement_state, effective_until, updated_by
292
+ FROM account_entitlements
293
+ WHERE tenant_id = ? AND account_id = ?
294
+ LIMIT 1
295
+ `).get(tenantId, 'expired-checkout-u1');
296
+ expect(entitlementRow).toEqual(expect.objectContaining({
297
+ entitlement_state: 'pending_plan_selection',
298
+ effective_until: null,
299
+ updated_by: 'checkout-expired'
300
+ }));
301
+ const billingRow = db.prepare(`
302
+ SELECT entitlement_state, tier, effective_until
303
+ FROM user_billing
304
+ WHERE tenant_id = ? AND user_id = ?
305
+ LIMIT 1
306
+ `).get(tenantId, 'expired-checkout-u1');
307
+ expect(billingRow).toEqual(expect.objectContaining({
308
+ entitlement_state: 'pending_plan_selection',
309
+ tier: '',
310
+ effective_until: null
152
311
  }));
153
312
  }
154
313
  finally {
155
- reopened.close();
314
+ core.close();
156
315
  fs.rmSync(projectRoot, { recursive: true, force: true });
157
316
  }
158
317
  });