@taskforcehq/taskforce 0.3.304 → 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 (138) hide show
  1. package/README.md +140 -247
  2. package/dist/TaskforceCore.js +165 -70
  3. package/dist/TaskforceCore.test.js +799 -90
  4. package/dist/compat/workspaceSyncCompat.js +6 -0
  5. package/dist/components/features/DocumentIndex.d.ts +1 -1
  6. package/dist/components/features/DocumentIndex.js +1 -1
  7. package/dist/components/features/DocumentViewer.d.ts +1 -1
  8. package/dist/components/features/DocumentViewer.js +32 -9
  9. package/dist/components/features/DocumentWorkspace.d.ts +5 -22
  10. package/dist/components/features/DocumentWorkspace.js +12 -155
  11. package/dist/components/features/DocumentWorkspace.test.js +193 -6
  12. package/dist/components/features/TaskSettings.js +29 -3
  13. package/dist/components/features/TaskSettings.test.js +42 -1
  14. package/dist/components/features/documentWorkspaceModel.d.ts +24 -0
  15. package/dist/components/features/documentWorkspaceModel.js +57 -0
  16. package/dist/components/features/useDocumentWorkspaceController.d.ts +34 -0
  17. package/dist/components/features/useDocumentWorkspaceController.js +113 -0
  18. package/dist/components/task/TaskForm.d.ts +8 -8
  19. package/dist/components/task/TaskForm.js +358 -221
  20. package/dist/components/task/TaskForm.test.js +456 -381
  21. package/dist/components/task/TaskKanban.js +2 -1
  22. package/dist/components/views/PlanComparisonPage.js +5 -8
  23. package/dist/components/views/PlanComparisonPage.test.js +51 -41
  24. package/dist/components/views/PlansPage.js +126 -84
  25. package/dist/components/views/PlansPage.test.js +308 -39
  26. package/dist/components/views/StandaloneLayout.js +28 -22
  27. package/dist/components/views/panels/FilterToolbar.test.js +6 -4
  28. package/dist/components/views/standalone/modals/AccountHubModal.d.ts +2 -14
  29. package/dist/components/views/standalone/modals/AccountHubModal.js +12 -14
  30. package/dist/components/views/standalone/modals/AccountHubModal.test.js +24 -1
  31. package/dist/config/envSchema.js +1 -1
  32. package/dist/core/AiProfileService.d.ts +6 -1
  33. package/dist/core/AiProfileService.js +161 -16
  34. package/dist/core/AiProfileService.test.js +3 -1
  35. package/dist/core/AiProfiles.test.js +200 -0
  36. package/dist/core/AuthTokenService.test.d.ts +1 -0
  37. package/dist/core/AuthTokenService.test.js +78 -0
  38. package/dist/core/EntitlementsPolicy.test.js +75 -13
  39. package/dist/core/PlanEntitlementService.d.ts +7 -1
  40. package/dist/core/PlanEntitlementService.js +117 -42
  41. package/dist/core/PlanVersionPolicy.test.js +19 -26
  42. package/dist/core/SignupMonetizationSettings.test.js +46 -21
  43. package/dist/core/SystemAdmin.test.js +212 -56
  44. package/dist/core/TaskTaxonomyValidation.test.js +53 -0
  45. package/dist/core/Taskforce.d.ts +3 -0
  46. package/dist/core/Taskforce.js +50 -10
  47. package/dist/core/WorkspacePlanMode.test.js +12 -12
  48. package/dist/core/shared.d.ts +2 -0
  49. package/dist/core/shared.js +11 -0
  50. package/dist/hooks/sync/auth.js +3 -1
  51. package/dist/hooks/sync/transfers.d.ts +2 -1
  52. package/dist/hooks/sync/transfers.js +8 -4
  53. package/dist/hooks/useSyncOrchestrator.d.ts +7 -4
  54. package/dist/hooks/useSyncOrchestrator.js +75 -7
  55. package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +80 -10
  56. package/dist/hooks/useTaskMutations.d.ts +2 -1
  57. package/dist/hooks/useTaskMutations.js +12 -1
  58. package/dist/hooks/useTaskMutations.test.js +30 -1
  59. package/dist/hooks/useTaskforce.d.ts +5 -2
  60. package/dist/hooks/useTaskforce.js +17 -5
  61. package/dist/hooks/useTaskforce.sync-behavior.test.js +78 -0
  62. package/dist/hooks/useWorkspaceSyncController.d.ts +3 -1
  63. package/dist/hooks/useWorkspaceSyncController.js +11 -1
  64. package/dist/hooks/useWorkspaceSyncController.test.js +1 -0
  65. package/dist/mcp/adminWorkspaceRegistrar.d.ts +2 -0
  66. package/dist/mcp/adminWorkspaceRegistrar.js +154 -0
  67. package/dist/mcp/collaborationRegistrar.d.ts +2 -0
  68. package/dist/mcp/collaborationRegistrar.js +71 -0
  69. package/dist/mcp/documentAssetRegistrar.d.ts +2 -0
  70. package/dist/mcp/documentAssetRegistrar.js +346 -0
  71. package/dist/mcp/runtime.js +2362 -3530
  72. package/dist/mcp/runtime.test.js +159 -0
  73. package/dist/mcp/taskPlanningRegistrar.d.ts +2 -0
  74. package/dist/mcp/taskPlanningRegistrar.js +418 -0
  75. package/dist/mcp/toolCatalog.d.ts +35 -0
  76. package/dist/mcp/toolCatalog.js +3 -0
  77. package/dist/server/routes/admin.d.ts +1 -5
  78. package/dist/server/routes/admin.js +138 -61
  79. package/dist/server/routes/annotatedAttachments.d.ts +1 -1
  80. package/dist/server/routes/annotatedAttachments.js +1 -1
  81. package/dist/server/routes/auth.d.ts +1 -4
  82. package/dist/server/routes/auth.js +23 -63
  83. package/dist/server/routes/authSupport.d.ts +30 -0
  84. package/dist/server/routes/authSupport.js +81 -0
  85. package/dist/server/routes/billing.d.ts +1 -1
  86. package/dist/server/routes/billing.js +104 -188
  87. package/dist/server/routes/billing.test.js +214 -86
  88. package/dist/server/routes/documentReviews.d.ts +1 -1
  89. package/dist/server/routes/documentReviews.js +1 -1
  90. package/dist/server/routes/documents.d.ts +4 -2
  91. package/dist/server/routes/documents.js +73 -7
  92. package/dist/server/routes/primitives.d.ts +11 -0
  93. package/dist/server/routes/primitives.js +73 -0
  94. package/dist/server/routes/resources.d.ts +1 -1
  95. package/dist/server/routes/resources.js +1 -1
  96. package/dist/server/routes/shared.d.ts +1 -1
  97. package/dist/server/routes/sync.d.ts +1 -1
  98. package/dist/server/routes/sync.js +1 -1
  99. package/dist/server/routes/tasks.d.ts +1 -1
  100. package/dist/server/routes/tasks.js +1 -1
  101. package/dist/server/routes/workspaces.d.ts +1 -1
  102. package/dist/server/routes/workspaces.js +1 -1
  103. package/dist/server/routes.d.ts +2 -9
  104. package/dist/server/routes.js +76 -162
  105. package/dist/server/routes.test.js +672 -30
  106. package/dist/sync/contentSyncState.js +16 -7
  107. package/dist/sync/syncApplyHandlers.test.js +2 -0
  108. package/dist/sync/syncService.d.ts +1 -0
  109. package/dist/sync/syncService.js +8 -0
  110. package/dist/sync/syncService.test.js +14 -0
  111. package/dist/sync/workspaceSyncModel.d.ts +3 -0
  112. package/dist/sync/workspaceSyncModel.js +12 -3
  113. package/dist/sync/workspaceSyncModel.test.js +34 -0
  114. package/dist/sync/workspaceSyncState.d.ts +2 -0
  115. package/dist/sync/workspaceSyncState.js +1 -0
  116. package/dist/ui/assets/{AgentsModule-BLWVbuKP.js → AgentsModule-N2MnQBZk.js} +1 -1
  117. package/dist/ui/assets/{AnnotatedAttachmentWorkspace-BlS-cqnl.js → AnnotatedAttachmentWorkspace-BG6P_GVW.js} +1 -1
  118. package/dist/ui/assets/DocumentWorkspace-B3nV_Gc5.css +1 -0
  119. package/dist/ui/assets/DocumentWorkspace-DRbDMwh8.js +252 -0
  120. package/dist/ui/assets/{InitiativesModule-BjR6iBf9.js → InitiativesModule-Dhm7M8e7.js} +1 -1
  121. package/dist/ui/assets/PlansPage-Cq0zXj3I.js +1 -0
  122. package/dist/ui/assets/TaskSettings-ln0aF7xc.js +9 -0
  123. package/dist/ui/assets/{WorkflowsModule-DnFqdUME.js → WorkflowsModule-BcS4afRn.js} +1 -1
  124. package/dist/ui/assets/{index-Ii0B0rTO.css → index-C2-OXZV7.css} +1 -1
  125. package/dist/ui/assets/index-DcSI5F86.js +6 -0
  126. package/dist/ui/assets/{vendor-icons-I7ZwG1z_.js → vendor-icons-BSUaRwF8.js} +1 -1
  127. package/dist/ui/index.html +3 -3
  128. package/dist/utils/commercialLifecyclePresentation.d.ts +19 -0
  129. package/dist/utils/commercialLifecyclePresentation.js +196 -0
  130. package/package.json +8 -1
  131. package/scripts/check-cloud-mcp.mjs +83 -0
  132. package/scripts/playwright-auth.sh +5 -1
  133. package/scripts/run-billing-performance-smoke.sh +24 -0
  134. package/dist/ui/assets/DocumentWorkspace-BH_6DF6x.js +0 -250
  135. package/dist/ui/assets/DocumentWorkspace-C_8T8oz-.css +0 -1
  136. package/dist/ui/assets/PlansPage-CSDQ4sLa.js +0 -1
  137. package/dist/ui/assets/TaskSettings-CEDy34Jo.js +0 -9
  138. package/dist/ui/assets/index-DXUdtH1B.js +0 -6
@@ -22,29 +22,29 @@ function openCore(projectRoot) {
22
22
  storagePath: path.join(projectRoot, '.taskforce'),
23
23
  tenantId: path.basename(projectRoot)
24
24
  });
25
- core.upsertPlanCatalog({ planId: 'starter', displayName: 'Starter', status: 'active' });
25
+ core.upsertPlanCatalog({ planId: 'free', displayName: 'Free', status: 'active' });
26
26
  core.updatePlanVersion({
27
- planId: 'starter',
27
+ planId: 'free',
28
28
  versionNumber: 1,
29
29
  seatLimit: 1
30
30
  });
31
31
  core.updatePlanVersionFeatures({
32
- planVersionId: 'starter-v1',
32
+ planVersionId: 'free-v1',
33
33
  features: [
34
34
  { featureKey: 'workspace.switching', access: 'enabled' }
35
35
  ]
36
36
  });
37
- insertFreePriceMapping(core, 'starter-v1');
37
+ insertFreePriceMapping(core, 'free-v1');
38
38
  return core;
39
39
  }
40
40
  describe('TaskforceCore onboarding policy', () => {
41
- it('defaults to auto_grant_free when a free personal plan version is available', () => {
41
+ it('fails closed to require_plan_selection when no explicit onboarding policy is configured', () => {
42
42
  const projectRoot = makeProjectRoot();
43
43
  const core = openCore(projectRoot);
44
44
  try {
45
45
  expect(core.getOnboardingPolicy()).toEqual({
46
- mode: 'auto_grant_free',
47
- planVersionId: 'starter-v1'
46
+ mode: 'require_plan_selection',
47
+ planVersionId: null
48
48
  });
49
49
  }
50
50
  finally {
@@ -59,14 +59,14 @@ describe('TaskforceCore onboarding policy', () => {
59
59
  storagePath: path.join(projectRoot, '.taskforce'),
60
60
  tenantId: path.basename(projectRoot)
61
61
  });
62
- core.upsertPlanCatalog({ planId: 'starter', displayName: 'Starter', status: 'active' });
62
+ core.upsertPlanCatalog({ planId: 'free', displayName: 'Free', status: 'active' });
63
63
  core.updatePlanVersion({
64
- planId: 'starter',
64
+ planId: 'free',
65
65
  versionNumber: 1,
66
66
  seatLimit: 1
67
67
  });
68
68
  core.updatePlanVersionFeatures({
69
- planVersionId: 'starter-v1',
69
+ planVersionId: 'free-v1',
70
70
  features: [
71
71
  { featureKey: 'workspace.switching', access: 'enabled' }
72
72
  ]
@@ -120,10 +120,10 @@ describe('TaskforceCore onboarding policy', () => {
120
120
  const projectRoot = makeProjectRoot();
121
121
  const core = openCore(projectRoot);
122
122
  try {
123
- core.upsertPlanCatalog({ planId: 'starter', displayName: 'Starter', status: 'archived' });
123
+ core.upsertPlanCatalog({ planId: 'free', displayName: 'Free', status: 'archived' });
124
124
  expect(core.validateOnboardingPolicy({
125
125
  mode: 'auto_grant_free',
126
- planVersionId: 'starter-v1'
126
+ planVersionId: 'free-v1'
127
127
  })).toEqual(expect.objectContaining({
128
128
  valid: false,
129
129
  code: 'ONBOARDING_PLAN_NOT_ENABLED'
@@ -138,10 +138,10 @@ describe('TaskforceCore onboarding policy', () => {
138
138
  const projectRoot = makeProjectRoot();
139
139
  const core = openCore(projectRoot);
140
140
  try {
141
- core.upsertPlanCatalog({ planId: 'collab', displayName: 'Collab', status: 'active' });
141
+ core.upsertPlanCatalog({ planId: 'team-5', displayName: 'Team 5', status: 'active' });
142
142
  expect(core.validateOnboardingPolicy({
143
143
  mode: 'auto_grant_free',
144
- planVersionId: 'collab-v1'
144
+ planVersionId: 'team-5-v1'
145
145
  })).toEqual(expect.objectContaining({
146
146
  valid: false,
147
147
  code: 'ONBOARDING_PLAN_MUST_BE_FREE'
@@ -166,6 +166,31 @@ describe('TaskforceCore onboarding policy', () => {
166
166
  fs.rmSync(projectRoot, { recursive: true, force: true });
167
167
  }
168
168
  });
169
+ it('requires plan selection during registration when no explicit onboarding policy is configured', () => {
170
+ const projectRoot = makeProjectRoot();
171
+ const core = openCore(projectRoot);
172
+ try {
173
+ const created = core.registerPasswordUser({
174
+ email: 'implicit-onboarding@example.com',
175
+ password: 'password123'
176
+ });
177
+ expect(created.planSelectionRequired).toBe(true);
178
+ expect(created.checkoutPending).toBe(false);
179
+ expect(core.getAccountEntitlement(created.userId)).toEqual(expect.objectContaining({
180
+ state: 'pending_plan_selection'
181
+ }));
182
+ expect(core.getAccountAccessStatus(created.userId)).toEqual(expect.objectContaining({
183
+ gate: 'plan_selection_required',
184
+ canAccessApp: false,
185
+ canAccessPlans: true,
186
+ planSelectionRequired: true
187
+ }));
188
+ }
189
+ finally {
190
+ core.close();
191
+ fs.rmSync(projectRoot, { recursive: true, force: true });
192
+ }
193
+ });
169
194
  it('allows backfill transitions to create pending plan selection entitlements', () => {
170
195
  const projectRoot = makeProjectRoot();
171
196
  const tenantId = path.basename(projectRoot);
@@ -191,7 +216,7 @@ describe('TaskforceCore onboarding policy', () => {
191
216
  `).run('pending-u1', tenantId, 'pending_plan_selection', now, now);
192
217
  core.transitionCanonicalCommercialState({
193
218
  accountId: 'pending-u1',
194
- planVersionId: 'starter-v1',
219
+ planVersionId: 'free-v1',
195
220
  state: 'pending_plan_selection',
196
221
  source: 'backfill',
197
222
  updatedBy: 'backfill',
@@ -220,7 +245,7 @@ describe('TaskforceCore onboarding policy', () => {
220
245
  try {
221
246
  const now = new Date().toISOString();
222
247
  const checkoutExpiresAt = new Date(Date.now() + (60 * 60 * 1000)).toISOString();
223
- core.upsertPlanCatalog({ planId: 'pro', displayName: 'Pro', status: 'active' });
248
+ core.upsertPlanCatalog({ planId: 'basic', displayName: 'Basic', status: 'active' });
224
249
  const db = core.getDatabaseAdapter();
225
250
  db.prepare(`
226
251
  INSERT INTO users (id, tenant_id, email, display_name, auth_provider, external_auth_id, is_disabled, created_at, updated_at)
@@ -228,11 +253,11 @@ describe('TaskforceCore onboarding policy', () => {
228
253
  `).run('checkout-u1', tenantId, 'checkout@example.com', 'Checkout', 'token', 'checkout-u1', now, now);
229
254
  core.transitionCanonicalCommercialState({
230
255
  accountId: 'checkout-u1',
231
- planVersionId: 'pro-v1',
256
+ planVersionId: 'basic-v1',
232
257
  state: 'checkout_pending',
233
258
  source: 'checkout-start',
234
259
  updatedBy: 'checkout-start',
235
- billingMirrorPlanId: 'pro',
260
+ billingMirrorPlanId: 'basic',
236
261
  effectiveAt: now,
237
262
  effectiveUntil: checkoutExpiresAt,
238
263
  createdAt: now,
@@ -258,7 +283,7 @@ describe('TaskforceCore onboarding policy', () => {
258
283
  try {
259
284
  const now = new Date().toISOString();
260
285
  const expiredAt = new Date(Date.now() - (60 * 1000)).toISOString();
261
- core.upsertPlanCatalog({ planId: 'pro', displayName: 'Pro', status: 'active' });
286
+ core.upsertPlanCatalog({ planId: 'basic', displayName: 'Basic', status: 'active' });
262
287
  const db = core.getDatabaseAdapter();
263
288
  db.prepare(`
264
289
  INSERT INTO users (id, tenant_id, email, display_name, auth_provider, external_auth_id, is_disabled, created_at, updated_at)
@@ -266,11 +291,11 @@ describe('TaskforceCore onboarding policy', () => {
266
291
  `).run('expired-checkout-u1', tenantId, 'expired@example.com', 'Expired Checkout', 'token', 'expired-checkout-u1', now, now);
267
292
  core.transitionCanonicalCommercialState({
268
293
  accountId: 'expired-checkout-u1',
269
- planVersionId: 'pro-v1',
294
+ planVersionId: 'basic-v1',
270
295
  state: 'checkout_pending',
271
296
  source: 'checkout-start',
272
297
  updatedBy: 'checkout-start',
273
- billingMirrorPlanId: 'pro',
298
+ billingMirrorPlanId: 'basic',
274
299
  effectiveAt: now,
275
300
  effectiveUntil: expiredAt,
276
301
  createdAt: now,
@@ -16,21 +16,34 @@ function insertFreePriceMapping(core, planVersionId) {
16
16
  `).run(core.getTenantId(), planVersionId, 'month', `free:${planVersionId}:month`, 'free', 1, 0, null, now, now);
17
17
  }
18
18
  function seedPlanCatalog(core) {
19
- core.upsertPlanCatalog({ planId: 'starter', displayName: 'Starter', status: 'active' });
20
- core.upsertPlanCatalog({ planId: 'collab', displayName: 'Collab', status: 'active' });
19
+ core.upsertPlanCatalog({ planId: 'free', displayName: 'Free', status: 'active' });
20
+ core.upsertPlanCatalog({ planId: 'basic', displayName: 'Basic', status: 'active' });
21
+ core.upsertPlanCatalog({ planId: 'team-5', displayName: 'Team 5', status: 'active' });
21
22
  core.updatePlanVersion({
22
- planId: 'collab',
23
+ planId: 'basic',
24
+ versionNumber: 1,
25
+ seatLimit: null
26
+ });
27
+ core.updatePlanVersion({
28
+ planId: 'team-5',
23
29
  versionNumber: 1,
24
30
  seatLimit: 5
25
31
  });
26
32
  core.updatePlanVersionFeatures({
27
- planVersionId: 'starter-v1',
33
+ planVersionId: 'free-v1',
34
+ features: [
35
+ { featureKey: 'workspace.switching', access: 'enabled' }
36
+ ]
37
+ });
38
+ core.updatePlanVersionFeatures({
39
+ planVersionId: 'basic-v1',
28
40
  features: [
41
+ { featureKey: 'context.uploads', access: 'enabled' },
29
42
  { featureKey: 'workspace.switching', access: 'enabled' }
30
43
  ]
31
44
  });
32
45
  core.updatePlanVersionFeatures({
33
- planVersionId: 'collab-v1',
46
+ planVersionId: 'team-5-v1',
34
47
  features: [
35
48
  { featureKey: 'collaboration.invites', access: 'enabled' },
36
49
  { featureKey: 'collaboration.team_management', access: 'enabled' },
@@ -38,7 +51,7 @@ function seedPlanCatalog(core) {
38
51
  { featureKey: 'workspace.switching', access: 'enabled' }
39
52
  ]
40
53
  });
41
- insertFreePriceMapping(core, 'starter-v1');
54
+ insertFreePriceMapping(core, 'free-v1');
42
55
  }
43
56
  function seedPlans(core) {
44
57
  seedPlanCatalog(core);
@@ -46,7 +59,7 @@ function seedPlans(core) {
46
59
  billing: {
47
60
  onboardingPolicy: {
48
61
  mode: 'auto_grant_free',
49
- planVersionId: 'starter-v1'
62
+ planVersionId: 'free-v1'
50
63
  }
51
64
  }
52
65
  });
@@ -78,7 +91,7 @@ describe('TaskforceCore system admin model', () => {
78
91
  LIMIT 1
79
92
  `).get('default', created.userId);
80
93
  expect(row?.user_id).toBe(created.userId);
81
- expect(row?.tier).toBe('starter');
94
+ expect(row?.tier).toBe('free');
82
95
  expect(row?.entitlement_state).toBe('active');
83
96
  }
84
97
  finally {
@@ -105,7 +118,7 @@ describe('TaskforceCore system admin model', () => {
105
118
  LIMIT 1
106
119
  `).get('default', created.userId);
107
120
  expect(row?.user_id).toBe(created.userId);
108
- expect(row?.tier).toBe('starter');
121
+ expect(row?.tier).toBe('free');
109
122
  expect(row?.entitlement_state).toBe('active');
110
123
  }
111
124
  finally {
@@ -118,14 +131,14 @@ describe('TaskforceCore system admin model', () => {
118
131
  const core = openCore(projectRoot, { seedDefaultPlans: true });
119
132
  try {
120
133
  core.createPlanVersion({
121
- planId: 'collab',
134
+ planId: 'team-5',
122
135
  versionNumber: 2,
123
136
  isDefault: true,
124
137
  seatLimit: 10,
125
138
  trialDays: 14
126
139
  });
127
140
  core.updatePlanVersionFeatures({
128
- planVersionId: 'collab-v2',
141
+ planVersionId: 'team-5-v2',
129
142
  features: [
130
143
  { featureKey: 'collaboration.invites', access: 'enabled' },
131
144
  { featureKey: 'collaboration.team_management', access: 'enabled' },
@@ -134,7 +147,7 @@ describe('TaskforceCore system admin model', () => {
134
147
  ]
135
148
  });
136
149
  core.createPlanVersion({
137
- planId: 'starter',
150
+ planId: 'free',
138
151
  versionNumber: 2,
139
152
  isDefault: false,
140
153
  seatLimit: null
@@ -143,7 +156,7 @@ describe('TaskforceCore system admin model', () => {
143
156
  billing: {
144
157
  onboardingPolicy: {
145
158
  mode: 'auto_start_trial',
146
- planVersionId: 'collab-v2'
159
+ planVersionId: 'team-5-v2'
147
160
  }
148
161
  }
149
162
  });
@@ -159,10 +172,10 @@ describe('TaskforceCore system admin model', () => {
159
172
  WHERE tenant_id = ? AND user_id = ?
160
173
  LIMIT 1
161
174
  `).get(core.getTenantId(), created.userId);
162
- expect(entitlement.planVersionId).toBe('collab-v2');
163
- expect(entitlement.planId).toBe('collab');
175
+ expect(entitlement.planVersionId).toBe('team-5-v2');
176
+ expect(entitlement.planId).toBe('team-5');
164
177
  expect(entitlement.state).toBe('trialing');
165
- expect(billing?.tier).toBe('collab');
178
+ expect(billing?.tier).toBe('team-5');
166
179
  expect(billing?.entitlement_state).toBe('trialing');
167
180
  }
168
181
  finally {
@@ -244,7 +257,7 @@ describe('TaskforceCore system admin model', () => {
244
257
  access = excluded.access,
245
258
  config_json = excluded.config_json,
246
259
  updated_at = excluded.updated_at
247
- `).run('default', 'collab-v1', 'documents.workspace_access', 'enabled', '{}', now, now);
260
+ `).run('default', 'team-5-v1', 'documents.workspace_access', 'enabled', '{}', now, now);
248
261
  }
249
262
  finally {
250
263
  core.close();
@@ -257,9 +270,9 @@ describe('TaskforceCore system admin model', () => {
257
270
  FROM plan_feature_matrix
258
271
  WHERE tenant_id = ? AND plan_version_id = ? AND feature_key = ?
259
272
  LIMIT 1
260
- `).get('default', 'collab-v1', 'documents.workspace_access');
273
+ `).get('default', 'team-5-v1', 'documents.workspace_access');
261
274
  expect(row).toBeUndefined();
262
- const snapshot = core.listPlanVersions('collab').find((version) => version.planVersionId === 'collab-v1');
275
+ const snapshot = core.listPlanVersions('team-5').find((version) => version.planVersionId === 'team-5-v1');
263
276
  expect(snapshot).toBeDefined();
264
277
  expect(snapshot.features['documents.workspace_access']).toBeUndefined();
265
278
  }
@@ -268,34 +281,120 @@ describe('TaskforceCore system admin model', () => {
268
281
  fs.rmSync(projectRoot, { recursive: true, force: true });
269
282
  }
270
283
  });
271
- it('assigns the explicitly selected plan version instead of the configured onboarding plan', () => {
284
+ it('keeps configured auto-grant onboarding plan when registration includes a selected plan version', () => {
272
285
  const projectRoot = makeProjectRoot();
273
286
  const core = openCore(projectRoot, { seedDefaultPlans: true });
274
287
  try {
275
- core.upsertPlanCatalog({ planId: 'free', displayName: 'Free', status: 'active' });
276
288
  core.updatePlanVersion({
277
- planId: 'free',
289
+ planId: 'basic',
278
290
  versionNumber: 1,
279
- seatLimit: 1
291
+ seatLimit: null
280
292
  });
281
- insertFreePriceMapping(core, 'free-v1');
282
- core.upsertPlanCatalog({ planId: 'starter', displayName: 'Starter', status: 'archived' });
283
293
  core.updateGlobalSettings({
284
294
  billing: {
285
295
  onboardingPolicy: {
286
296
  mode: 'auto_grant_free',
287
- planVersionId: 'starter-v1'
297
+ planVersionId: 'free-v1'
288
298
  }
289
299
  }
290
300
  });
291
301
  const created = core.registerPasswordUser({
292
302
  email: 'selected-plan@example.com',
293
303
  password: 'password123',
294
- planVersionId: 'free-v1'
304
+ planVersionId: 'basic-v1'
305
+ });
306
+ expect(created.planSelectionRequired).toBe(false);
307
+ expect(core.getAccountEntitlement(created.userId)).toMatchObject({
308
+ planVersionId: 'free-v1',
309
+ planId: 'free',
310
+ state: 'active'
311
+ });
312
+ }
313
+ finally {
314
+ core.close();
315
+ fs.rmSync(projectRoot, { recursive: true, force: true });
316
+ }
317
+ });
318
+ it('uses the explicitly selected plan version when onboarding requires plan selection', () => {
319
+ const projectRoot = makeProjectRoot();
320
+ const core = openCore(projectRoot, { seedDefaultPlans: true });
321
+ try {
322
+ core.updatePlanVersion({
323
+ planId: 'basic',
324
+ versionNumber: 1,
325
+ seatLimit: null
326
+ });
327
+ core.updateGlobalSettings({
328
+ billing: {
329
+ onboardingPolicy: {
330
+ mode: 'require_plan_selection',
331
+ planVersionId: null
332
+ }
333
+ }
334
+ });
335
+ const created = core.registerPasswordUser({
336
+ email: 'selected-plan-required@example.com',
337
+ password: 'password123',
338
+ planVersionId: 'basic-v1'
295
339
  });
296
340
  expect(created.planSelectionRequired).toBe(false);
297
341
  expect(core.getAccountEntitlement(created.userId)).toMatchObject({
342
+ planVersionId: 'basic-v1',
343
+ planId: 'basic',
344
+ state: 'checkout_pending'
345
+ });
346
+ }
347
+ finally {
348
+ core.close();
349
+ fs.rmSync(projectRoot, { recursive: true, force: true });
350
+ }
351
+ });
352
+ it('auto-heals stranded legacy personal free entitlements onto the active default free version', () => {
353
+ const projectRoot = makeProjectRoot();
354
+ const core = openCore(projectRoot, { seedDefaultPlans: true });
355
+ try {
356
+ core.upsertPlanCatalog({ planId: 'free', displayName: 'Free', status: 'active' });
357
+ insertFreePriceMapping(core, 'free-v1');
358
+ core.createPlanVersion({
359
+ planId: 'free',
360
+ versionNumber: 2,
361
+ isDefault: true,
362
+ seatLimit: 1
363
+ });
364
+ insertFreePriceMapping(core, 'free-v2');
365
+ core.updateGlobalSettings({
366
+ billing: {
367
+ onboardingPolicy: {
368
+ mode: 'require_plan_selection',
369
+ planVersionId: null
370
+ }
371
+ }
372
+ });
373
+ const created = core.registerPasswordUser({
374
+ email: 'legacy-free@example.com',
375
+ password: 'password123',
376
+ planVersionId: 'free-v1'
377
+ });
378
+ core.updateAccountEntitlement({
379
+ accountId: created.userId,
298
380
  planVersionId: 'free-v1',
381
+ entitlementState: 'canceled',
382
+ actorUserId: created.userId,
383
+ actorRole: 'user',
384
+ reason: 'legacy-free-v1-canceled'
385
+ });
386
+ expect(core.getAccountAccessStatus(created.userId)).toMatchObject({
387
+ gate: 'ok',
388
+ hasEntitlement: true,
389
+ canAccessApp: true,
390
+ entitlement: {
391
+ planVersionId: 'free-v2',
392
+ planId: 'free',
393
+ state: 'active'
394
+ }
395
+ });
396
+ expect(core.getAccountEntitlement(created.userId)).toMatchObject({
397
+ planVersionId: 'free-v2',
299
398
  planId: 'free',
300
399
  state: 'active'
301
400
  });
@@ -305,6 +404,61 @@ describe('TaskforceCore system admin model', () => {
305
404
  fs.rmSync(projectRoot, { recursive: true, force: true });
306
405
  }
307
406
  });
407
+ it('does not auto-heal canceled accounts already on the current default free version', () => {
408
+ const projectRoot = makeProjectRoot();
409
+ const core = openCore(projectRoot, { seedDefaultPlans: true });
410
+ try {
411
+ core.upsertPlanCatalog({ planId: 'free', displayName: 'Free', status: 'active' });
412
+ insertFreePriceMapping(core, 'free-v1');
413
+ core.createPlanVersion({
414
+ planId: 'free',
415
+ versionNumber: 2,
416
+ isDefault: true,
417
+ seatLimit: 1
418
+ });
419
+ insertFreePriceMapping(core, 'free-v2');
420
+ core.updateGlobalSettings({
421
+ billing: {
422
+ onboardingPolicy: {
423
+ mode: 'require_plan_selection',
424
+ planVersionId: null
425
+ }
426
+ }
427
+ });
428
+ const created = core.registerPasswordUser({
429
+ email: 'current-default-free@example.com',
430
+ password: 'password123',
431
+ planVersionId: 'free-v2'
432
+ });
433
+ core.updateAccountEntitlement({
434
+ accountId: created.userId,
435
+ planVersionId: 'free-v2',
436
+ entitlementState: 'canceled',
437
+ actorUserId: created.userId,
438
+ actorRole: 'user',
439
+ reason: 'intentional-free-v2-cancel'
440
+ });
441
+ expect(core.getAccountAccessStatus(created.userId)).toMatchObject({
442
+ gate: 'missing_entitlement',
443
+ hasEntitlement: true,
444
+ canAccessApp: false,
445
+ entitlement: {
446
+ planVersionId: 'free-v2',
447
+ planId: 'free',
448
+ state: 'canceled'
449
+ }
450
+ });
451
+ expect(core.getAccountEntitlement(created.userId)).toMatchObject({
452
+ planVersionId: 'free-v2',
453
+ planId: 'free',
454
+ state: 'canceled'
455
+ });
456
+ }
457
+ finally {
458
+ core.close();
459
+ fs.rmSync(projectRoot, { recursive: true, force: true });
460
+ }
461
+ });
308
462
  it('treats accounts without an entitlement or pending-selection marker as missing entitlement', () => {
309
463
  const projectRoot = makeProjectRoot();
310
464
  const core = openCore(projectRoot, { seedDefaultPlans: true });
@@ -344,7 +498,7 @@ describe('TaskforceCore system admin model', () => {
344
498
  UPDATE plan_versions
345
499
  SET is_default = 0
346
500
  WHERE tenant_id = ? AND plan_id = ?
347
- `).run('default', 'starter');
501
+ `).run('default', 'free');
348
502
  }
349
503
  finally {
350
504
  initial.close();
@@ -357,10 +511,10 @@ describe('TaskforceCore system admin model', () => {
357
511
  FROM plan_versions
358
512
  WHERE tenant_id = ? AND plan_id = ? AND plan_version_id = ?
359
513
  LIMIT 1
360
- `).get('default', 'starter', 'starter-v1');
361
- expect(row?.plan_version_id).toBe('starter-v1');
514
+ `).get('default', 'free', 'free-v1');
515
+ expect(row?.plan_version_id).toBe('free-v1');
362
516
  expect(row?.is_default).toBe(1);
363
- expect(reopened.getRequiredDefaultPlanVersionId('starter')).toBe('starter-v1');
517
+ expect(reopened.getRequiredDefaultPlanVersionId('free')).toBe('free-v1');
364
518
  }
365
519
  finally {
366
520
  reopened.close();
@@ -405,7 +559,7 @@ describe('TaskforceCore system admin model', () => {
405
559
  UPDATE plan_versions
406
560
  SET seat_limit = NULL, updated_at = ?
407
561
  WHERE tenant_id = ? AND plan_version_id = ?
408
- `).run(now, 'default', 'collab-v1');
562
+ `).run(now, 'default', 'team-5-v1');
409
563
  db.prepare(`
410
564
  INSERT INTO plan_feature_matrix (tenant_id, plan_version_id, feature_key, access, config_json, created_at, updated_at)
411
565
  VALUES (?, ?, 'collaboration.seat_limit', 'limited', ?, ?, ?)
@@ -413,14 +567,14 @@ describe('TaskforceCore system admin model', () => {
413
567
  access = excluded.access,
414
568
  config_json = excluded.config_json,
415
569
  updated_at = excluded.updated_at
416
- `).run('default', 'collab-v1', JSON.stringify({ seat_limit: 7 }), now, now);
570
+ `).run('default', 'team-5-v1', JSON.stringify({ seat_limit: 7 }), now, now);
417
571
  }
418
572
  finally {
419
573
  initial.close();
420
574
  }
421
575
  const reopened = openCore(projectRoot, { seedDefaultPlans: false });
422
576
  try {
423
- const version = reopened.getPlanVersionFeatures('collab', 'collab-v1');
577
+ const version = reopened.getPlanVersionFeatures('team-5', 'team-5-v1');
424
578
  expect(version.seatLimit).toBe(7);
425
579
  expect(version.features['collaboration.seat_limit']?.config || {}).toEqual({});
426
580
  }
@@ -453,13 +607,15 @@ describe('TaskforceCore system admin model', () => {
453
607
  const reopened = openCore(projectRoot, { seedDefaultPlans: false });
454
608
  try {
455
609
  const db = reopened.db;
610
+ const normalizedPersonalVersionId = reopened.getDefaultPlanVersionIdByMode('personal');
611
+ const normalizedBillingMirror = reopened.getBillingMirrorProfileForPlanVersion(normalizedPersonalVersionId);
456
612
  const row = db.prepare(`
457
613
  SELECT tier, entitlement_state
458
614
  FROM user_billing
459
615
  WHERE tenant_id = ? AND user_id = ?
460
616
  LIMIT 1
461
617
  `).get('default', legacyUserId);
462
- expect(row?.tier).toBe('starter');
618
+ expect(row?.tier).toBe(normalizedBillingMirror.planId);
463
619
  expect(row?.entitlement_state).toBe('active');
464
620
  }
465
621
  finally {
@@ -490,11 +646,11 @@ describe('TaskforceCore system admin model', () => {
490
646
  db.prepare(`
491
647
  INSERT INTO plan_catalog (plan_id, tenant_id, display_name, status, created_at, updated_at)
492
648
  VALUES (?, ?, ?, 'active', ?, ?)
493
- `).run('enterprise', 'default', 'Enterprise', now, now);
649
+ `).run('agency', 'default', 'Agency', now, now);
494
650
  db.prepare(`
495
651
  INSERT INTO plan_versions (plan_version_id, tenant_id, plan_id, version_number, is_default, seat_limit, metadata_json, created_at, updated_at)
496
652
  VALUES (?, ?, ?, 1, 1, 25, '{}', ?, ?)
497
- `).run('enterprise-v1', 'default', 'enterprise', now, now);
653
+ `).run('agency-v1', 'default', 'agency', now, now);
498
654
  }
499
655
  finally {
500
656
  initial.close();
@@ -502,8 +658,8 @@ describe('TaskforceCore system admin model', () => {
502
658
  const reopened = openCore(projectRoot, { seedDefaultPlans: false });
503
659
  try {
504
660
  const plans = reopened.listPlanCatalog();
505
- expect(plans.map((plan) => plan.planId)).toEqual(['enterprise']);
506
- expect(reopened.getDefaultPlanVersionIdByMode('personal')).toBe('enterprise-v1');
661
+ expect(plans.map((plan) => plan.planId)).toEqual(['agency']);
662
+ expect(reopened.getDefaultPlanVersionIdByMode('personal')).toBe('agency-v1');
507
663
  }
508
664
  finally {
509
665
  reopened.close();
@@ -613,13 +769,13 @@ describe('TaskforceCore system admin model', () => {
613
769
  password: 'password123'
614
770
  });
615
771
  core.createPlanVersion({
616
- planId: 'starter',
772
+ planId: 'free',
617
773
  versionNumber: 2,
618
774
  isDefault: true
619
775
  });
620
776
  core.updateAccountEntitlement({
621
777
  accountId: member.userId,
622
- planVersionId: 'starter-v2',
778
+ planVersionId: 'free-v2',
623
779
  entitlementState: 'active',
624
780
  actorUserId: admin.userId,
625
781
  actorRole: 'system_admin'
@@ -741,7 +897,7 @@ describe('TaskforceCore system admin model', () => {
741
897
  ON CONFLICT (user_id) DO UPDATE SET
742
898
  stripe_customer_id = excluded.stripe_customer_id,
743
899
  updated_at = excluded.updated_at
744
- `).run(member.userId, 'default', 'cus_test_123', 'active', 'starter', now, now);
900
+ `).run(member.userId, 'default', 'cus_test_123', 'active', 'basic', now, now);
745
901
  expect(() => core.purgeSystemUser({
746
902
  targetUserId: member.userId,
747
903
  actorUserId: admin.userId,
@@ -758,16 +914,16 @@ describe('TaskforceCore system admin model', () => {
758
914
  const core = openCore(projectRoot, { seedDefaultPlans: true });
759
915
  try {
760
916
  const plan = core.upsertPlanCatalog({
761
- planId: 'enterprise',
762
- displayName: 'Enterprise',
917
+ planId: 'agency',
918
+ displayName: 'Agency',
763
919
  status: 'active'
764
920
  });
765
- expect(plan.planId).toBe('enterprise');
766
- const versions = core.listPlanVersions('enterprise');
921
+ expect(plan.planId).toBe('agency');
922
+ const versions = core.listPlanVersions('agency');
767
923
  expect(versions).toHaveLength(1);
768
924
  expect(versions[0]).toMatchObject({
769
- planVersionId: 'enterprise-v1',
770
- planId: 'enterprise',
925
+ planVersionId: 'agency-v1',
926
+ planId: 'agency',
771
927
  versionNumber: 1,
772
928
  isDefault: true,
773
929
  seatLimit: null
@@ -783,24 +939,24 @@ describe('TaskforceCore system admin model', () => {
783
939
  const core = openCore(projectRoot, { seedDefaultPlans: true });
784
940
  try {
785
941
  core.upsertPlanCatalog({
786
- planId: 'enterprise',
787
- displayName: 'Enterprise',
942
+ planId: 'agency',
943
+ displayName: 'Agency',
788
944
  status: 'active'
789
945
  });
790
946
  core.createPlanVersion({
791
- planId: 'enterprise',
947
+ planId: 'agency',
792
948
  versionNumber: 2,
793
949
  isDefault: true,
794
950
  seatLimit: 25
795
951
  });
796
952
  core.upsertPlanCatalog({
797
- planId: 'enterprise',
798
- displayName: 'Enterprise Plus',
953
+ planId: 'agency',
954
+ displayName: 'Agency Plus',
799
955
  status: 'archived'
800
956
  });
801
- const versions = core.listPlanVersions('enterprise');
802
- expect(versions.map((version) => version.planVersionId)).toEqual(['enterprise-v2', 'enterprise-v1']);
803
- expect(versions.find((version) => version.planVersionId === 'enterprise-v1')).toMatchObject({
957
+ const versions = core.listPlanVersions('agency');
958
+ expect(versions.map((version) => version.planVersionId)).toEqual(['agency-v2', 'agency-v1']);
959
+ expect(versions.find((version) => version.planVersionId === 'agency-v1')).toMatchObject({
804
960
  isDefault: false
805
961
  });
806
962
  }