@taskforcehq/taskforce 0.3.301 → 0.3.303

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 (171) hide show
  1. package/dist/Taskforce.module.css +37 -2
  2. package/dist/TaskforceCore.filter-persistence.test.js +1 -1
  3. package/dist/TaskforceCore.js +72 -33
  4. package/dist/TaskforceCore.test.js +848 -130
  5. package/dist/components/features/TaskSettings.js +14 -82
  6. package/dist/components/features/TaskSettings.test.js +33 -70
  7. package/dist/components/features/settings/TaxonomyLibraryManager.js +8 -2
  8. package/dist/components/features/settings/TaxonomyLibraryManager.test.js +1 -1
  9. package/dist/components/task/TaskActionHeader.js +6 -3
  10. package/dist/components/task/TaskCard.js +6 -3
  11. package/dist/components/task/TaskForm.js +2 -2
  12. package/dist/components/task/TaskForm.test.js +41 -2
  13. package/dist/components/task/TaskKanban.test.js +17 -0
  14. package/dist/components/ui/ReferenceBadgeButton.js +1 -1
  15. package/dist/components/views/PlansPage.d.ts +3 -1
  16. package/dist/components/views/PlansPage.js +109 -49
  17. package/dist/components/views/PlansPage.test.d.ts +1 -0
  18. package/dist/components/views/PlansPage.test.js +100 -0
  19. package/dist/components/views/StandaloneLayout.d.ts +1 -0
  20. package/dist/components/views/StandaloneLayout.js +473 -256
  21. package/dist/components/views/panels/PlanningDrawer.js +18 -1
  22. package/dist/components/views/standalone/modals/AccountHubModal.d.ts +48 -0
  23. package/dist/components/views/standalone/modals/AccountHubModal.js +36 -0
  24. package/dist/components/views/standalone/modals/AccountHubModal.test.d.ts +1 -0
  25. package/dist/components/views/standalone/modals/AccountHubModal.test.js +26 -0
  26. package/dist/components/views/standalone/modals/CreateWorkspaceConfirmModal.d.ts +9 -0
  27. package/dist/components/views/standalone/modals/CreateWorkspaceConfirmModal.js +8 -0
  28. package/dist/components/views/standalone/modals/EditProfileModal.d.ts +26 -0
  29. package/dist/components/views/standalone/modals/EditProfileModal.js +10 -0
  30. package/dist/components/views/standalone/modals/HelpModal.d.ts +9 -0
  31. package/dist/components/views/standalone/modals/HelpModal.js +8 -0
  32. package/dist/components/views/standalone/modals/ScheduleWarningModal.d.ts +13 -0
  33. package/dist/components/views/standalone/modals/ScheduleWarningModal.js +6 -0
  34. package/dist/components/views/standalone/modals/SyncStatusModal.d.ts +61 -0
  35. package/dist/components/views/standalone/modals/SyncStatusModal.js +38 -0
  36. package/dist/components/views/standalone/modals/TeamManagementModal.d.ts +64 -0
  37. package/dist/components/views/standalone/modals/TeamManagementModal.js +10 -0
  38. package/dist/components/views/standalone/modals/types.d.ts +2 -0
  39. package/dist/components/views/standalone/modals/types.js +1 -0
  40. package/dist/components/views/teamManagementAccess.d.ts +1 -1
  41. package/dist/components/views/teamManagementAccess.js +2 -2
  42. package/dist/components/views/teamManagementAccess.test.js +3 -3
  43. package/dist/config/deployment.js +2 -2
  44. package/dist/config/deployment.test.js +5 -3
  45. package/dist/core/AiProfileService.js +7 -2
  46. package/dist/core/AiProfiles.test.js +19 -0
  47. package/dist/core/EntitlementsPolicy.test.js +2 -2
  48. package/dist/core/GlobalSettingsService.js +78 -18
  49. package/dist/core/PlanEntitlementService.d.ts +29 -5
  50. package/dist/core/PlanEntitlementService.js +297 -219
  51. package/dist/core/SignupMonetizationSettings.test.js +173 -49
  52. package/dist/core/SyncReconciliationService.d.ts +2 -0
  53. package/dist/core/SyncReconciliationService.js +3 -0
  54. package/dist/core/SystemAdmin.test.js +186 -96
  55. package/dist/core/TaskAuditTimeline.test.js +40 -0
  56. package/dist/core/Taskforce.d.ts +100 -10
  57. package/dist/core/Taskforce.ids.test.js +133 -0
  58. package/dist/core/Taskforce.js +459 -127
  59. package/dist/core/Taskforce.mcpAuth.test.js +0 -2
  60. package/dist/core/UserProfileAvatarDrafts.test.d.ts +1 -0
  61. package/dist/core/UserProfileAvatarDrafts.test.js +111 -0
  62. package/dist/core/taskReferencePolicy.d.ts +28 -0
  63. package/dist/core/taskReferencePolicy.js +189 -0
  64. package/dist/core/types.d.ts +13 -12
  65. package/dist/hooks/tasks/useTaskFormActions.js +3 -0
  66. package/dist/hooks/useSyncOrchestrator.js +17 -3
  67. package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +97 -5
  68. package/dist/hooks/useTaskData.js +3 -2
  69. package/dist/hooks/useTaskData.test.js +66 -0
  70. package/dist/hooks/useTaskforce.d.ts +8 -0
  71. package/dist/hooks/useTaskforce.js +72 -19
  72. package/dist/hooks/useTaskforce.sync-behavior.test.js +185 -6
  73. package/dist/mcp/aiProfileBootstrap.d.ts +29 -0
  74. package/dist/mcp/aiProfileBootstrap.js +80 -0
  75. package/dist/mcp/aiProfileBootstrap.test.d.ts +1 -0
  76. package/dist/mcp/aiProfileBootstrap.test.js +68 -0
  77. package/dist/mcp/clientIntegrations.d.ts +2 -8
  78. package/dist/mcp/clientIntegrations.js +21 -40
  79. package/dist/mcp/clientIntegrations.test.js +28 -2
  80. package/dist/mcp/clientProfileDefaults.d.ts +12 -0
  81. package/dist/mcp/clientProfileDefaults.js +37 -0
  82. package/dist/mcp/runtime.d.ts +10 -7
  83. package/dist/mcp/runtime.js +131 -48
  84. package/dist/mcp/runtime.test.js +321 -83
  85. package/dist/migrations/billingSchemaParity.test.js +3 -0
  86. package/dist/migrations/taskSchemaMigrations.js +68 -1
  87. package/dist/qaOpen.test.d.ts +1 -0
  88. package/dist/qaOpen.test.js +56 -0
  89. package/dist/server/index.cookieProxy.test.js +2 -0
  90. package/dist/server/index.d.ts +1 -0
  91. package/dist/server/index.js +17 -1
  92. package/dist/server/index.test.js +15 -1
  93. package/dist/server/mockStripe.d.ts +1 -0
  94. package/dist/server/mockStripe.js +2 -0
  95. package/dist/server/routes/admin.js +105 -45
  96. package/dist/server/routes/auth.d.ts +6 -0
  97. package/dist/server/routes/auth.js +505 -30
  98. package/dist/server/routes/billing.js +517 -24
  99. package/dist/server/routes/billing.test.js +967 -45
  100. package/dist/server/routes/documents.js +6 -1
  101. package/dist/server/routes/shared.d.ts +1 -1
  102. package/dist/server/routes/shared.js +16 -6
  103. package/dist/server/routes/sync.integration.test.js +290 -0
  104. package/dist/server/routes/sync.js +234 -1922
  105. package/dist/server/routes/tasks.js +18 -4
  106. package/dist/server/routes.js +29 -22
  107. package/dist/server/routes.test.js +639 -82
  108. package/dist/shared/runtimeContract.d.ts +1 -1
  109. package/dist/sync/cloudSyncApi.d.ts +1 -0
  110. package/dist/sync/collaborationSyncPayload.d.ts +17 -0
  111. package/dist/sync/collaborationSyncPayload.js +131 -0
  112. package/dist/sync/collaborationSyncPayload.test.d.ts +1 -0
  113. package/dist/sync/collaborationSyncPayload.test.js +104 -0
  114. package/dist/sync/collaborationSyncState.d.ts +15 -0
  115. package/dist/sync/collaborationSyncState.js +60 -0
  116. package/dist/sync/contentSyncPayload.d.ts +45 -0
  117. package/dist/sync/contentSyncPayload.js +104 -0
  118. package/dist/sync/contentSyncPayload.test.d.ts +1 -0
  119. package/dist/sync/contentSyncPayload.test.js +104 -0
  120. package/dist/sync/contentSyncState.d.ts +98 -0
  121. package/dist/sync/contentSyncState.js +1004 -0
  122. package/dist/sync/planningSyncPayload.d.ts +9 -0
  123. package/dist/sync/planningSyncPayload.js +82 -0
  124. package/dist/sync/planningSyncPayload.test.d.ts +1 -0
  125. package/dist/sync/planningSyncPayload.test.js +68 -0
  126. package/dist/sync/syncApplyHandlers.d.ts +83 -0
  127. package/dist/sync/syncApplyHandlers.js +445 -0
  128. package/dist/sync/syncService.d.ts +1 -0
  129. package/dist/sync/syncService.js +21 -8
  130. package/dist/sync/taskSyncPayload.d.ts +33 -0
  131. package/dist/sync/taskSyncPayload.js +126 -0
  132. package/dist/sync/taskSyncPayload.test.d.ts +1 -0
  133. package/dist/sync/taskSyncPayload.test.js +54 -0
  134. package/dist/sync/workspacePullFeed.d.ts +38 -0
  135. package/dist/sync/workspacePullFeed.js +349 -0
  136. package/dist/sync/workspacePullFeed.test.d.ts +1 -0
  137. package/dist/sync/workspacePullFeed.test.js +278 -0
  138. package/dist/sync/workspaceSyncModel.d.ts +4 -1
  139. package/dist/sync/workspaceSyncModel.js +9 -2
  140. package/dist/sync/workspaceSyncModel.test.js +29 -0
  141. package/dist/test/setup.js +1 -1
  142. package/dist/types.d.ts +1 -0
  143. package/dist/ui/assets/{AgentsModule-Bdq3T1Ts.js → AgentsModule-CpsTmrIW.js} +1 -1
  144. package/dist/ui/assets/{AnnotatedAttachmentWorkspace-jKUVo-PJ.js → AnnotatedAttachmentWorkspace-C_TmXZwA.js} +1 -1
  145. package/dist/ui/assets/{DocumentWorkspace-DuQa-uIC.js → DocumentWorkspace-C3GyzrWm.js} +2 -2
  146. package/dist/ui/assets/{InitiativesModule-C8Pi2VTk.js → InitiativesModule-4-Rh2K2n.js} +1 -1
  147. package/dist/ui/assets/PlansPage-BP7AYOqr.js +1 -0
  148. package/dist/ui/assets/TaskSettings-BOC5F6Ag.js +8 -0
  149. package/dist/ui/assets/{TaskSettings-BwB9NtmJ.css → TaskSettings-CnIBL_Eb.css} +1 -1
  150. package/dist/ui/assets/{WorkflowsModule-b6TLuHwJ.js → WorkflowsModule-CGk9s3NJ.js} +1 -1
  151. package/dist/ui/assets/index-CLIMgR6g.js +6 -0
  152. package/dist/ui/assets/index-DneETxcu.css +1 -0
  153. package/dist/ui/assets/{vendor-icons-QZyhEwgT.js → vendor-icons-pWocEipT.js} +1 -1
  154. package/dist/ui/index.html +3 -3
  155. package/dist/utils/accountProfileSummaryCache.d.ts +31 -0
  156. package/dist/utils/accountProfileSummaryCache.js +97 -0
  157. package/dist/utils/accountProfileSummaryCache.test.d.ts +1 -0
  158. package/dist/utils/accountProfileSummaryCache.test.js +63 -0
  159. package/dist/utils/taskActivity.js +17 -4
  160. package/dist/utils/taskActivity.test.js +27 -0
  161. package/dist/utils/taskNormalization.js +10 -2
  162. package/dist/utils/taskReferences.d.ts +11 -1
  163. package/dist/utils/taskReferences.js +33 -1
  164. package/package.json +4 -3
  165. package/scripts/qa-open-lib.mjs +175 -0
  166. package/scripts/qa-open-lib.mjs.d.ts +49 -0
  167. package/scripts/qa-open.mjs +352 -0
  168. package/dist/ui/assets/PlansPage-B6BDECSi.js +0 -1
  169. package/dist/ui/assets/TaskSettings-DUJgRTPU.js +0 -8
  170. package/dist/ui/assets/index-BiiF1cLQ.css +0 -1
  171. package/dist/ui/assets/index-xBWLPOEb.js +0 -6
@@ -4,7 +4,17 @@ 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
+ }
9
+ function insertFreePriceMapping(core, planVersionId) {
10
+ const db = core.db;
11
+ const tenantId = core.getTenantId();
12
+ const now = new Date().toISOString();
13
+ db.prepare(`
14
+ INSERT OR IGNORE INTO billing_price_mappings (
15
+ tenant_id, plan_version_id, billing_interval, stripe_price_id, pricing_type, active, unit_amount, currency, created_at, updated_at
16
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
17
+ `).run(tenantId, planVersionId, 'month', `free:${planVersionId}:month`, 'free', 1, 0, null, now, now);
8
18
  }
9
19
  function openCore(projectRoot) {
10
20
  const core = new TaskforceCore({
@@ -12,31 +22,24 @@ function openCore(projectRoot) {
12
22
  storagePath: path.join(projectRoot, '.taskforce'),
13
23
  tenantId: path.basename(projectRoot)
14
24
  });
15
- const db = core.db;
16
- const now = new Date().toISOString();
17
25
  core.upsertPlanCatalog({ planId: 'starter', displayName: 'Starter' });
18
- db.prepare(`
19
- INSERT OR IGNORE INTO billing_price_mappings (
20
- tenant_id, plan_version_id, billing_interval, stripe_price_id, pricing_type, active, unit_amount, currency, created_at, updated_at
21
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
22
- `).run(path.basename(projectRoot), 'starter-v1', 'month', 'free:starter-v1:month', 'free', 1, 0, null, now, now);
23
- core.upsertPlanCatalog({ planId: 'starter', displayName: 'Starter', status: 'active' });
24
26
  core.updatePlanVersionFeatures({
25
27
  planVersionId: 'starter-v1',
26
28
  features: [
27
29
  { featureKey: 'workspace.switching', access: 'enabled' }
28
30
  ]
29
31
  });
32
+ insertFreePriceMapping(core, 'starter-v1');
30
33
  return core;
31
34
  }
32
- describe('TaskforceCore signup monetization settings', () => {
33
- it('normalizes defaults to auto_assign_default with no version id', () => {
35
+ describe('TaskforceCore onboarding policy', () => {
36
+ it('defaults to require_plan_selection with no plan version', () => {
34
37
  const projectRoot = makeProjectRoot();
35
38
  const core = openCore(projectRoot);
36
39
  try {
37
- expect(core.getSignupMonetizationSettings()).toEqual({
38
- strategy: 'auto_assign_default',
39
- defaultSignupPlanVersionId: null
40
+ expect(core.getOnboardingPolicy()).toEqual({
41
+ mode: 'require_plan_selection',
42
+ planVersionId: null
40
43
  });
41
44
  }
42
45
  finally {
@@ -44,16 +47,16 @@ describe('TaskforceCore signup monetization settings', () => {
44
47
  fs.rmSync(projectRoot, { recursive: true, force: true });
45
48
  }
46
49
  });
47
- it('fails validation for auto_assign_default without a default version id', () => {
50
+ it('fails validation for auto_grant_free without a plan version id', () => {
48
51
  const projectRoot = makeProjectRoot();
49
52
  const core = openCore(projectRoot);
50
53
  try {
51
- expect(core.validateSignupMonetizationSettings({
52
- strategy: 'auto_assign_default',
53
- defaultSignupPlanVersionId: null
54
+ expect(core.validateOnboardingPolicy({
55
+ mode: 'auto_grant_free',
56
+ planVersionId: null
54
57
  })).toEqual(expect.objectContaining({
55
58
  valid: false,
56
- code: 'SIGNUP_PLAN_VERSION_REQUIRED'
59
+ code: 'ONBOARDING_PLAN_VERSION_REQUIRED'
57
60
  }));
58
61
  }
59
62
  finally {
@@ -65,12 +68,12 @@ describe('TaskforceCore signup monetization settings', () => {
65
68
  const projectRoot = makeProjectRoot();
66
69
  const core = openCore(projectRoot);
67
70
  try {
68
- expect(core.validateSignupMonetizationSettings({
69
- strategy: 'auto_assign_default',
70
- defaultSignupPlanVersionId: 'missing-v9'
71
+ expect(core.validateOnboardingPolicy({
72
+ mode: 'auto_grant_free',
73
+ planVersionId: 'missing-v9'
71
74
  })).toEqual(expect.objectContaining({
72
75
  valid: false,
73
- code: 'SIGNUP_PLAN_VERSION_NOT_FOUND'
76
+ code: 'ONBOARDING_PLAN_VERSION_NOT_FOUND'
74
77
  }));
75
78
  }
76
79
  finally {
@@ -83,12 +86,12 @@ describe('TaskforceCore signup monetization settings', () => {
83
86
  const core = openCore(projectRoot);
84
87
  try {
85
88
  core.upsertPlanCatalog({ planId: 'starter', displayName: 'Starter', status: 'archived' });
86
- expect(core.validateSignupMonetizationSettings({
87
- strategy: 'auto_assign_default',
88
- defaultSignupPlanVersionId: 'starter-v1'
89
+ expect(core.validateOnboardingPolicy({
90
+ mode: 'auto_grant_free',
91
+ planVersionId: 'starter-v1'
89
92
  })).toEqual(expect.objectContaining({
90
93
  valid: false,
91
- code: 'SIGNUP_PLAN_NOT_ENABLED'
94
+ code: 'ONBOARDING_PLAN_NOT_ENABLED'
92
95
  }));
93
96
  }
94
97
  finally {
@@ -96,13 +99,31 @@ describe('TaskforceCore signup monetization settings', () => {
96
99
  fs.rmSync(projectRoot, { recursive: true, force: true });
97
100
  }
98
101
  });
99
- it('passes validation for plan_selection_required without a default version id', () => {
102
+ it('fails validation when auto_grant_free points at a version without free pricing', () => {
100
103
  const projectRoot = makeProjectRoot();
101
104
  const core = openCore(projectRoot);
102
105
  try {
103
- expect(core.validateSignupMonetizationSettings({
104
- strategy: 'plan_selection_required',
105
- defaultSignupPlanVersionId: null
106
+ core.upsertPlanCatalog({ planId: 'collab', displayName: 'Collab', status: 'active' });
107
+ expect(core.validateOnboardingPolicy({
108
+ mode: 'auto_grant_free',
109
+ planVersionId: 'collab-v1'
110
+ })).toEqual(expect.objectContaining({
111
+ valid: false,
112
+ code: 'ONBOARDING_PLAN_MUST_BE_FREE'
113
+ }));
114
+ }
115
+ finally {
116
+ core.close();
117
+ fs.rmSync(projectRoot, { recursive: true, force: true });
118
+ }
119
+ });
120
+ it('passes validation for require_plan_selection without a plan version id', () => {
121
+ const projectRoot = makeProjectRoot();
122
+ const core = openCore(projectRoot);
123
+ try {
124
+ expect(core.validateOnboardingPolicy({
125
+ mode: 'require_plan_selection',
126
+ planVersionId: null
106
127
  })).toEqual({ valid: true });
107
128
  }
108
129
  finally {
@@ -110,19 +131,21 @@ describe('TaskforceCore signup monetization settings', () => {
110
131
  fs.rmSync(projectRoot, { recursive: true, force: true });
111
132
  }
112
133
  });
113
- it('does not backfill canonical entitlements for pending plan selection accounts', () => {
134
+ it('allows backfill transitions to create pending plan selection entitlements', () => {
114
135
  const projectRoot = makeProjectRoot();
115
136
  const tenantId = path.basename(projectRoot);
116
- const initial = openCore(projectRoot);
137
+ const core = openCore(projectRoot);
117
138
  try {
118
139
  const now = new Date().toISOString();
119
- initial.updateGlobalSettings({
120
- auth: {
121
- signupMonetizationStrategy: 'plan_selection_required',
122
- defaultSignupPlanVersionId: null
140
+ core.updateGlobalSettings({
141
+ billing: {
142
+ onboardingPolicy: {
143
+ mode: 'require_plan_selection',
144
+ planVersionId: null
145
+ }
123
146
  }
124
147
  });
125
- const db = initial.getDatabaseAdapter();
148
+ const db = core.getDatabaseAdapter();
126
149
  db.prepare(`
127
150
  INSERT INTO users (id, tenant_id, email, display_name, auth_provider, external_auth_id, is_disabled, created_at, updated_at)
128
151
  VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?)
@@ -131,28 +154,129 @@ describe('TaskforceCore signup monetization settings', () => {
131
154
  INSERT INTO user_billing (user_id, tenant_id, entitlement_state, created_at, updated_at)
132
155
  VALUES (?, ?, ?, ?, ?)
133
156
  `).run('pending-u1', tenantId, 'pending_plan_selection', now, now);
157
+ core.transitionCanonicalCommercialState({
158
+ accountId: 'pending-u1',
159
+ planVersionId: 'starter-v1',
160
+ state: 'pending_plan_selection',
161
+ source: 'backfill',
162
+ updatedBy: 'backfill',
163
+ billingMirrorPlanId: null,
164
+ effectiveAt: now,
165
+ createdAt: now,
166
+ updatedAt: now
167
+ });
168
+ expect(core.hasAccountEntitlement('pending-u1')).toBe(true);
169
+ expect(core.getAccountAccessStatus('pending-u1')).toEqual(expect.objectContaining({
170
+ gate: 'plan_selection_required',
171
+ canAccessApp: false,
172
+ canAccessPlans: true,
173
+ planSelectionRequired: true
174
+ }));
175
+ }
176
+ finally {
177
+ core.close();
178
+ fs.rmSync(projectRoot, { recursive: true, force: true });
179
+ }
180
+ });
181
+ it('treats a live checkout_pending entitlement as checkout recovery state', () => {
182
+ const projectRoot = makeProjectRoot();
183
+ const tenantId = path.basename(projectRoot);
184
+ const core = openCore(projectRoot);
185
+ try {
186
+ const now = new Date().toISOString();
187
+ const checkoutExpiresAt = new Date(Date.now() + (60 * 60 * 1000)).toISOString();
188
+ core.upsertPlanCatalog({ planId: 'pro', displayName: 'Pro', status: 'active' });
189
+ const db = core.getDatabaseAdapter();
190
+ db.prepare(`
191
+ INSERT INTO users (id, tenant_id, email, display_name, auth_provider, external_auth_id, is_disabled, created_at, updated_at)
192
+ VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?)
193
+ `).run('checkout-u1', tenantId, 'checkout@example.com', 'Checkout', 'token', 'checkout-u1', now, now);
194
+ core.transitionCanonicalCommercialState({
195
+ accountId: 'checkout-u1',
196
+ planVersionId: 'pro-v1',
197
+ state: 'checkout_pending',
198
+ source: 'checkout-start',
199
+ updatedBy: 'checkout-start',
200
+ billingMirrorPlanId: 'pro',
201
+ effectiveAt: now,
202
+ effectiveUntil: checkoutExpiresAt,
203
+ createdAt: now,
204
+ updatedAt: now
205
+ });
206
+ expect(core.getAccountAccessStatus('checkout-u1')).toEqual(expect.objectContaining({
207
+ gate: 'checkout_pending',
208
+ canAccessApp: false,
209
+ canAccessPlans: true,
210
+ planSelectionRequired: false,
211
+ message: 'Complete checkout to continue.'
212
+ }));
134
213
  }
135
214
  finally {
136
- initial.close();
215
+ core.close();
216
+ fs.rmSync(projectRoot, { recursive: true, force: true });
137
217
  }
138
- const reopened = openCore(projectRoot);
218
+ });
219
+ it('treats an expired checkout_pending entitlement as requiring plan reselection', () => {
220
+ const projectRoot = makeProjectRoot();
221
+ const tenantId = path.basename(projectRoot);
222
+ const core = openCore(projectRoot);
139
223
  try {
140
- reopened.updateGlobalSettings({
141
- auth: {
142
- signupMonetizationStrategy: 'plan_selection_required',
143
- defaultSignupPlanVersionId: null
144
- }
224
+ const now = new Date().toISOString();
225
+ const expiredAt = new Date(Date.now() - (60 * 1000)).toISOString();
226
+ core.upsertPlanCatalog({ planId: 'pro', displayName: 'Pro', status: 'active' });
227
+ const db = core.getDatabaseAdapter();
228
+ db.prepare(`
229
+ INSERT INTO users (id, tenant_id, email, display_name, auth_provider, external_auth_id, is_disabled, created_at, updated_at)
230
+ VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?)
231
+ `).run('expired-checkout-u1', tenantId, 'expired@example.com', 'Expired Checkout', 'token', 'expired-checkout-u1', now, now);
232
+ core.transitionCanonicalCommercialState({
233
+ accountId: 'expired-checkout-u1',
234
+ planVersionId: 'pro-v1',
235
+ state: 'checkout_pending',
236
+ source: 'checkout-start',
237
+ updatedBy: 'checkout-start',
238
+ billingMirrorPlanId: 'pro',
239
+ effectiveAt: now,
240
+ effectiveUntil: expiredAt,
241
+ createdAt: now,
242
+ updatedAt: now
145
243
  });
146
- expect(reopened.hasAccountEntitlement('pending-u1')).toBe(false);
147
- expect(reopened.getAccountAccessStatus('pending-u1')).toEqual(expect.objectContaining({
244
+ expect(core.getAccountAccessStatus('expired-checkout-u1')).toEqual(expect.objectContaining({
148
245
  gate: 'plan_selection_required',
149
246
  canAccessApp: false,
150
247
  canAccessPlans: true,
151
- planSelectionRequired: true
248
+ planSelectionRequired: true,
249
+ message: 'Choose a plan to continue.'
250
+ }));
251
+ expect(core.getAccountEntitlement('expired-checkout-u1')).toEqual(expect.objectContaining({
252
+ state: 'pending_plan_selection',
253
+ effectiveUntil: null
254
+ }));
255
+ const entitlementRow = db.prepare(`
256
+ SELECT entitlement_state, effective_until, updated_by
257
+ FROM account_entitlements
258
+ WHERE tenant_id = ? AND account_id = ?
259
+ LIMIT 1
260
+ `).get(tenantId, 'expired-checkout-u1');
261
+ expect(entitlementRow).toEqual(expect.objectContaining({
262
+ entitlement_state: 'pending_plan_selection',
263
+ effective_until: null,
264
+ updated_by: 'checkout-expired'
265
+ }));
266
+ const billingRow = db.prepare(`
267
+ SELECT entitlement_state, tier, effective_until
268
+ FROM user_billing
269
+ WHERE tenant_id = ? AND user_id = ?
270
+ LIMIT 1
271
+ `).get(tenantId, 'expired-checkout-u1');
272
+ expect(billingRow).toEqual(expect.objectContaining({
273
+ entitlement_state: 'pending_plan_selection',
274
+ tier: '',
275
+ effective_until: null
152
276
  }));
153
277
  }
154
278
  finally {
155
- reopened.close();
279
+ core.close();
156
280
  fs.rmSync(projectRoot, { recursive: true, force: true });
157
281
  }
158
282
  });
@@ -21,6 +21,7 @@ export interface SyncReconciliationServiceDeps {
21
21
  persistBackup?: boolean;
22
22
  workspaceId?: string;
23
23
  allowClientProvidedId?: boolean;
24
+ preferIncomingReferenceNumberOnConflict?: boolean;
24
25
  skipRecompute?: boolean;
25
26
  syncUpdatedAt?: string;
26
27
  actorRef?: string;
@@ -28,6 +29,7 @@ export interface SyncReconciliationServiceDeps {
28
29
  }) => TaskItem;
29
30
  updateTask: (id: string, updates: Partial<TaskItem>, workspaceId?: string, options?: {
30
31
  persistBackup?: boolean;
32
+ preferIncomingReferenceNumberOnConflict?: boolean;
31
33
  skipRecompute?: boolean;
32
34
  syncUpdatedAt?: string;
33
35
  actorRef?: string;
@@ -198,6 +198,7 @@ export function applyWorkspaceSyncSnapshotService(deps, input, workspaceId) {
198
198
  createdBy: sourceTask.createdBy === undefined ? existing?.createdBy : sourceTask.createdBy,
199
199
  assignee: sourceTask.assignee === undefined ? existing?.assignee : sourceTask.assignee,
200
200
  referenceNumber: sourceTask.referenceNumber === undefined ? existing?.referenceNumber : sourceTask.referenceNumber,
201
+ localReferenceNumber: sourceTask.referenceNumber === undefined ? existing?.localReferenceNumber : null,
201
202
  scheduledDate: sourceTask.scheduledDate === undefined ? existing?.scheduledDate : sourceTask.scheduledDate,
202
203
  dueDate: sourceTask.dueDate === undefined ? existing?.dueDate : sourceTask.dueDate,
203
204
  scheduledWeekKey: sourceTask.scheduledWeekKey === undefined ? existing?.scheduledWeekKey : sourceTask.scheduledWeekKey,
@@ -228,6 +229,7 @@ export function applyWorkspaceSyncSnapshotService(deps, input, workspaceId) {
228
229
  persistBackup: false,
229
230
  workspaceId: normalizedWorkspaceId,
230
231
  allowClientProvidedId: true,
232
+ preferIncomingReferenceNumberOnConflict: true,
231
233
  skipRecompute: true,
232
234
  syncUpdatedAt: incomingUpdatedAt,
233
235
  skipTaskEvents: true
@@ -244,6 +246,7 @@ export function applyWorkspaceSyncSnapshotService(deps, input, workspaceId) {
244
246
  try {
245
247
  updatedTask = deps.updateTask(taskId, taskPatch, normalizedWorkspaceId, {
246
248
  persistBackup: false,
249
+ preferIncomingReferenceNumberOnConflict: true,
247
250
  skipRecompute: true,
248
251
  syncUpdatedAt: incomingUpdatedAt,
249
252
  skipTaskEvents: true