@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
@@ -31,7 +31,8 @@ import { normalizePriorityDefinitions as normalizePriorityDefinitionsInternal, n
31
31
  import { DEFAULT_CATEGORY_LABEL, DEFAULT_CATEGORY_VALUE, DEFAULT_TYPE_VALUE, } from '../shared/taxonomyDefaults.js';
32
32
  import { cloneDefaultWorkflowDefinitions, DEFAULT_WORKFLOW_VALUE, } from '../shared/workflowDefaults.js';
33
33
  import { getBuiltInTaxonomyLibraryPack, getDefaultStarterTaxonomyLibraryPack, getEmergencyStarterTaxonomyLibraryPack } from '../shared/taxonomyLibrary.js';
34
- import { formatTaskReference, parseTaskReference, } from '../utils/taskReferences.js';
34
+ import { parseTaskReference, } from '../utils/taskReferences.js';
35
+ import { allocateNextLocalTaskReferenceNumber as allocateNextLocalTaskReferenceNumberPolicy, deriveTaskReferenceLabel, allocateNextTaskReferenceNumber as allocateNextTaskReferenceNumberPolicy, getLocalTaskReferenceSequenceValue as getLocalTaskReferenceSequenceValuePolicy, getMaxLocalTaskReferenceNumber as getMaxLocalTaskReferenceNumberPolicy, getMaxTaskReferenceNumber as getMaxTaskReferenceNumberPolicy, getTaskByLocalReferenceNumber as getTaskByLocalReferenceNumberPolicy, getTaskByReferenceNumber as getTaskByReferenceNumberPolicy, getTaskReferenceSequenceValue as getTaskReferenceSequenceValuePolicy, persistLocalTaskReferenceSequence as persistLocalTaskReferenceSequencePolicy, persistTaskReferenceSequence as persistTaskReferenceSequencePolicy, relocateTaskReferenceConflict as relocateTaskReferenceConflictPolicy, reserveTaskReferenceNumber as reserveTaskReferenceNumberPolicy, resolveTaskIdentifier as resolveTaskIdentifierPolicy, } from './taskReferencePolicy.js';
35
36
  import { formatInitiativeReference, formatWorkstreamReference, } from '../utils/planningReferences.js';
36
37
  import { getDocumentReferenceLabel } from '../utils/documentReferences.js';
37
38
  import { getImageReferenceLabel } from '../utils/imageReferences.js';
@@ -1109,7 +1110,7 @@ export class TaskforceCore {
1109
1110
  : path.resolve(this.projectRoot, storagePath);
1110
1111
  this.dbPath = path.resolve(this.basePath, 'taskforce.db');
1111
1112
  this.configFile = path.resolve(this.basePath, 'config.json');
1112
- this.globalSettingsDbPath = path.resolve(os.homedir(), '.tasknexus', 'taskforce-global.db');
1113
+ this.globalSettingsDbPath = path.resolve(os.homedir(), '.taskforce', 'taskforce-global.db');
1113
1114
  this.ensureDirectories();
1114
1115
  this.migrateLegacyGlobalSettingsDbIfNeeded();
1115
1116
  // Initialize Database Adapter (SQLite default; Postgres scaffolded)
@@ -1174,7 +1175,6 @@ export class TaskforceCore {
1174
1175
  writeGlobalConfig: (config) => this.writeGlobalConfig(config),
1175
1176
  });
1176
1177
  this.planEntitlementService.repairPlanCatalogDefaultsIfNeeded();
1177
- this.planEntitlementService.repairSignupMonetizationSettingsIfNeeded();
1178
1178
  this.planEntitlementService.normalizeLegacyPlanDataIfNeeded();
1179
1179
  this.planEntitlementService.backfillAccountEntitlements();
1180
1180
  this.repairDetachedAnnotatedAttachmentSessions();
@@ -1208,11 +1208,14 @@ export class TaskforceCore {
1208
1208
  upsertCanonicalEntitlementRecord(input) {
1209
1209
  return this.planEntitlementService.upsertCanonicalEntitlementRecord(input);
1210
1210
  }
1211
+ transitionCanonicalCommercialState(input) {
1212
+ return this.planEntitlementService.transitionCanonicalCommercialState(input);
1213
+ }
1211
1214
  resolveWorkspaceLimitForUser(userId) {
1212
1215
  return this.planEntitlementService.resolveWorkspaceLimitForUser(userId);
1213
1216
  }
1214
- resolveRegistrationPlanAssignment(selectedPlanVersionIdRaw) {
1215
- return this.planEntitlementService.resolveRegistrationPlanAssignment(selectedPlanVersionIdRaw);
1217
+ resolveRegistrationOnboardingOutcome(selectedPlanVersionIdRaw) {
1218
+ return this.planEntitlementService.resolveRegistrationOnboardingOutcome(selectedPlanVersionIdRaw);
1216
1219
  }
1217
1220
  createUserBillingRecord(userId, options) {
1218
1221
  return this.planEntitlementService.createUserBillingRecord(userId, options);
@@ -1269,7 +1272,6 @@ export class TaskforceCore {
1269
1272
  if (fs.existsSync(this.globalSettingsDbPath))
1270
1273
  return;
1271
1274
  const candidates = [
1272
- path.resolve(os.homedir(), '.taskforce', 'taskforce-global.db'),
1273
1275
  path.resolve(this.basePath, '_global', 'taskforce-global.db')
1274
1276
  ];
1275
1277
  for (const legacyPath of candidates) {
@@ -1297,6 +1299,7 @@ export class TaskforceCore {
1297
1299
  tenant_id TEXT NOT NULL DEFAULT 'default',
1298
1300
  workspace_id TEXT NOT NULL DEFAULT 'default',
1299
1301
  reference_number INTEGER,
1302
+ local_reference_number INTEGER,
1300
1303
  title TEXT NOT NULL,
1301
1304
  description TEXT,
1302
1305
  status TEXT NOT NULL,
@@ -1331,6 +1334,13 @@ export class TaskforceCore {
1331
1334
  PRIMARY KEY (tenant_id, workspace_id)
1332
1335
  );
1333
1336
 
1337
+ CREATE TABLE IF NOT EXISTS workspace_local_task_reference_sequences (
1338
+ tenant_id TEXT NOT NULL DEFAULT 'default',
1339
+ workspace_id TEXT NOT NULL DEFAULT 'default',
1340
+ next_reference_number INTEGER NOT NULL DEFAULT 0,
1341
+ PRIMARY KEY (tenant_id, workspace_id)
1342
+ );
1343
+
1334
1344
  CREATE TABLE IF NOT EXISTS initiatives (
1335
1345
  id TEXT PRIMARY KEY,
1336
1346
  tenant_id TEXT NOT NULL DEFAULT 'default',
@@ -2590,11 +2600,11 @@ export class TaskforceCore {
2590
2600
  const config = this.readConfig(workspaceId, userId) || {};
2591
2601
  return typeof config.jsonBackupEnabled === 'boolean';
2592
2602
  }
2593
- getSignupMonetizationSettings() {
2594
- return this.planEntitlementService.getSignupMonetizationSettings();
2603
+ getOnboardingPolicy() {
2604
+ return this.planEntitlementService.getOnboardingPolicy();
2595
2605
  }
2596
- validateSignupMonetizationSettings(input) {
2597
- return this.planEntitlementService.validateSignupMonetizationSettings(input);
2606
+ validateOnboardingPolicy(input) {
2607
+ return this.planEntitlementService.validateOnboardingPolicy(input);
2598
2608
  }
2599
2609
  updateGlobalSettings(settings) {
2600
2610
  updateGlobalSettingsService(this.getGlobalSettingsServiceDeps(), settings);
@@ -2788,10 +2798,12 @@ export class TaskforceCore {
2788
2798
  const dueDate = this.sanitizeDateOnly(row.due_date);
2789
2799
  const scheduledWeekKey = row.scheduled_week_key || (scheduledDate ? this.dateOnlyToIsoWeekKey(scheduledDate) : null);
2790
2800
  const referenceNumber = this.sanitizeTaskReferenceNumber(row.reference_number);
2801
+ const localReferenceNumber = this.sanitizeTaskReferenceNumber(row.local_reference_number);
2791
2802
  return {
2792
2803
  id: taskId,
2793
2804
  referenceNumber,
2794
- referenceLabel: formatTaskReference(referenceNumber),
2805
+ localReferenceNumber,
2806
+ referenceLabel: deriveTaskReferenceLabel({ referenceNumber, localReferenceNumber }),
2795
2807
  title: row.title,
2796
2808
  description: row.description,
2797
2809
  status: this.normalizeStatusValue(row.status),
@@ -2879,31 +2891,24 @@ export class TaskforceCore {
2879
2891
  const normalized = Math.floor(numeric);
2880
2892
  return normalized > 0 ? normalized : null;
2881
2893
  }
2894
+ getTaskReferencePolicyDeps() {
2895
+ return {
2896
+ db: this.db,
2897
+ tenantId: this.tenantId,
2898
+ normalizeWorkspaceId: this.normalizeWorkspaceId.bind(this),
2899
+ sanitizeTaskReferenceNumber: this.sanitizeTaskReferenceNumber.bind(this),
2900
+ rowToTask: this.rowToTask.bind(this),
2901
+ getTask: this.getTask.bind(this),
2902
+ };
2903
+ }
2882
2904
  getTaskByReferenceNumber(referenceNumber, workspaceId) {
2883
- const normalizedReferenceNumber = this.sanitizeTaskReferenceNumber(referenceNumber);
2884
- if (!normalizedReferenceNumber)
2885
- return null;
2886
- const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
2887
- const row = this.db.prepare(`
2888
- SELECT * FROM tasks
2889
- WHERE tenant_id = ? AND workspace_id = ? AND reference_number = ?
2890
- LIMIT 1
2891
- `).get(this.tenantId, normalizedWorkspaceId, normalizedReferenceNumber);
2892
- if (!row)
2893
- return null;
2894
- return this.rowToTask(row);
2905
+ return getTaskByReferenceNumberPolicy(this.getTaskReferencePolicyDeps(), referenceNumber, workspaceId);
2906
+ }
2907
+ getTaskByLocalReferenceNumber(referenceNumber, workspaceId) {
2908
+ return getTaskByLocalReferenceNumberPolicy(this.getTaskReferencePolicyDeps(), referenceNumber, workspaceId);
2895
2909
  }
2896
2910
  resolveTaskIdentifier(value, workspaceId) {
2897
- const normalizedValue = String(value || '').trim();
2898
- if (!normalizedValue)
2899
- return null;
2900
- const referenceNumber = parseTaskReference(normalizedValue);
2901
- if (referenceNumber) {
2902
- const exactReferenceMatch = this.getTaskByReferenceNumber(referenceNumber, workspaceId);
2903
- if (exactReferenceMatch)
2904
- return exactReferenceMatch;
2905
- }
2906
- return this.getTask(normalizedValue, workspaceId);
2911
+ return resolveTaskIdentifierPolicy(this.getTaskReferencePolicyDeps(), value, workspaceId);
2907
2912
  }
2908
2913
  getInitiative(id, workspaceId) {
2909
2914
  const normalizedId = String(id || '').trim();
@@ -3234,42 +3239,35 @@ export class TaskforceCore {
3234
3239
  });
3235
3240
  return this.listChecklistItems(normalizedTaskId, normalizedWorkspaceId);
3236
3241
  }
3237
- reserveTaskReferenceNumber(workspaceId, preferredReferenceNumber) {
3238
- const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
3239
- const preferred = this.sanitizeTaskReferenceNumber(preferredReferenceNumber);
3240
- if (preferred) {
3241
- const existing = this.db.prepare(`
3242
- SELECT id
3243
- FROM tasks
3244
- WHERE tenant_id = ? AND workspace_id = ? AND reference_number = ?
3245
- LIMIT 1
3246
- `).get(this.tenantId, normalizedWorkspaceId, preferred);
3247
- if (!existing?.id) {
3248
- this.db.prepare(`
3249
- INSERT INTO workspace_task_reference_sequences (tenant_id, workspace_id, next_reference_number)
3250
- VALUES (?, ?, ?)
3251
- ON CONFLICT (tenant_id, workspace_id) DO UPDATE SET
3252
- next_reference_number = CASE
3253
- WHEN workspace_task_reference_sequences.next_reference_number < EXCLUDED.next_reference_number
3254
- THEN EXCLUDED.next_reference_number
3255
- ELSE workspace_task_reference_sequences.next_reference_number
3256
- END
3257
- `).run(this.tenantId, normalizedWorkspaceId, preferred);
3258
- return preferred;
3259
- }
3260
- }
3261
- const row = this.db.prepare(`
3262
- INSERT INTO workspace_task_reference_sequences (tenant_id, workspace_id, next_reference_number)
3263
- VALUES (?, ?, 1)
3264
- ON CONFLICT (tenant_id, workspace_id) DO UPDATE SET
3265
- next_reference_number = workspace_task_reference_sequences.next_reference_number + 1
3266
- RETURNING next_reference_number
3267
- `).get(this.tenantId, normalizedWorkspaceId);
3268
- const referenceNumber = this.sanitizeTaskReferenceNumber(row?.next_reference_number);
3269
- if (!referenceNumber) {
3270
- throw new TaskforceRuleError('Failed to allocate task reference number', 500, 'TASK_REFERENCE_ALLOC_FAILED');
3271
- }
3272
- return referenceNumber;
3242
+ getTaskReferenceSequenceValue(workspaceId) {
3243
+ return getTaskReferenceSequenceValuePolicy(this.getTaskReferencePolicyDeps(), workspaceId);
3244
+ }
3245
+ getLocalTaskReferenceSequenceValue(workspaceId) {
3246
+ return getLocalTaskReferenceSequenceValuePolicy(this.getTaskReferencePolicyDeps(), workspaceId);
3247
+ }
3248
+ getMaxTaskReferenceNumber(workspaceId, excludeTaskId) {
3249
+ return getMaxTaskReferenceNumberPolicy(this.getTaskReferencePolicyDeps(), workspaceId, excludeTaskId);
3250
+ }
3251
+ getMaxLocalTaskReferenceNumber(workspaceId, excludeTaskId) {
3252
+ return getMaxLocalTaskReferenceNumberPolicy(this.getTaskReferencePolicyDeps(), workspaceId, excludeTaskId);
3253
+ }
3254
+ persistTaskReferenceSequence(workspaceId, referenceNumber) {
3255
+ persistTaskReferenceSequencePolicy(this.getTaskReferencePolicyDeps(), workspaceId, referenceNumber);
3256
+ }
3257
+ persistLocalTaskReferenceSequence(workspaceId, referenceNumber) {
3258
+ persistLocalTaskReferenceSequencePolicy(this.getTaskReferencePolicyDeps(), workspaceId, referenceNumber);
3259
+ }
3260
+ allocateNextTaskReferenceNumber(workspaceId, excludeTaskId) {
3261
+ return allocateNextTaskReferenceNumberPolicy(this.getTaskReferencePolicyDeps(), workspaceId, excludeTaskId);
3262
+ }
3263
+ allocateNextLocalTaskReferenceNumber(workspaceId, excludeTaskId) {
3264
+ return allocateNextLocalTaskReferenceNumberPolicy(this.getTaskReferencePolicyDeps(), workspaceId, excludeTaskId);
3265
+ }
3266
+ relocateTaskReferenceConflict(workspaceId, referenceNumber, claimantTaskId, updatedAt) {
3267
+ return relocateTaskReferenceConflictPolicy(this.getTaskReferencePolicyDeps(), workspaceId, referenceNumber, claimantTaskId, updatedAt);
3268
+ }
3269
+ reserveTaskReferenceNumber(workspaceId, preferredReferenceNumber, currentTaskId) {
3270
+ return reserveTaskReferenceNumberPolicy(this.getTaskReferencePolicyDeps(), workspaceId, preferredReferenceNumber, currentTaskId);
3273
3271
  }
3274
3272
  reserveGenericReferenceNumber(params) {
3275
3273
  const normalizedWorkspaceId = this.normalizeWorkspaceId(params.workspaceId);
@@ -4060,7 +4058,7 @@ export class TaskforceCore {
4060
4058
  if (!normalizedUserId || normalizedUserId === 'anonymous')
4061
4059
  return null;
4062
4060
  const row = this.db.prepare(`
4063
- SELECT id, email, display_name, system_role, COALESCE(beta_access, 1) AS beta_access
4061
+ SELECT id, email, display_name, avatar_url, system_role, COALESCE(beta_access, 1) AS beta_access
4064
4062
  FROM users
4065
4063
  WHERE tenant_id = ? AND id = ?
4066
4064
  LIMIT 1
@@ -4071,13 +4069,17 @@ export class TaskforceCore {
4071
4069
  userId: row.id,
4072
4070
  email: row.email,
4073
4071
  displayName: row.display_name || null,
4072
+ avatarUrl: row.avatar_url || null,
4074
4073
  systemRole: this.normalizeSystemRole(row.system_role, 'user'),
4075
4074
  betaAccess: row.beta_access === undefined || row.beta_access === null ? true : Boolean(row.beta_access)
4076
4075
  };
4077
4076
  }
4078
- updateUserDisplayName(userId, displayName) {
4077
+ updateUserProfile(userId, input) {
4079
4078
  const normalizedUserId = String(userId || '').trim();
4080
- const trimmedDisplayName = String(displayName || '').trim();
4079
+ const trimmedDisplayName = String(input.displayName || '').trim();
4080
+ const normalizedAvatarUrl = input.avatarUrl === undefined
4081
+ ? undefined
4082
+ : (typeof input.avatarUrl === 'string' && input.avatarUrl.trim().length > 0 ? input.avatarUrl.trim() : null);
4081
4083
  if (!normalizedUserId)
4082
4084
  return null;
4083
4085
  if (!trimmedDisplayName) {
@@ -4086,15 +4088,145 @@ export class TaskforceCore {
4086
4088
  if (trimmedDisplayName.length > 120) {
4087
4089
  throw new TaskforceRuleError('Display name must be 120 characters or fewer', 400, 'DISPLAY_NAME_TOO_LONG');
4088
4090
  }
4089
- const result = this.db.prepare(`
4090
- UPDATE users
4091
- SET display_name = ?, updated_at = ?
4092
- WHERE tenant_id = ? AND id = ?
4093
- `).run(trimmedDisplayName, new Date().toISOString(), this.tenantId, normalizedUserId);
4091
+ const now = new Date().toISOString();
4092
+ const result = normalizedAvatarUrl === undefined
4093
+ ? this.db.prepare(`
4094
+ UPDATE users
4095
+ SET display_name = ?, updated_at = ?
4096
+ WHERE tenant_id = ? AND id = ?
4097
+ `).run(trimmedDisplayName, now, this.tenantId, normalizedUserId)
4098
+ : this.db.prepare(`
4099
+ UPDATE users
4100
+ SET display_name = ?, avatar_url = ?, updated_at = ?
4101
+ WHERE tenant_id = ? AND id = ?
4102
+ `).run(trimmedDisplayName, normalizedAvatarUrl, now, this.tenantId, normalizedUserId);
4094
4103
  if (Number(result?.changes || 0) <= 0)
4095
4104
  return null;
4096
4105
  return this.resolveUserIdentity(normalizedUserId);
4097
4106
  }
4107
+ updateUserDisplayName(userId, displayName) {
4108
+ return this.updateUserProfile(userId, { displayName });
4109
+ }
4110
+ createUserAvatarUploadDraft(input) {
4111
+ const draftId = String(input.draftId || '').trim();
4112
+ const userId = String(input.userId || '').trim();
4113
+ const storageKey = String(input.storageKey || '').trim();
4114
+ const mimeType = String(input.mimeType || 'application/octet-stream').trim() || 'application/octet-stream';
4115
+ const originalFilename = String(input.originalFilename || '').trim() || 'avatar';
4116
+ const sizeBytes = Number.isFinite(Number(input.sizeBytes)) ? Math.max(0, Math.floor(Number(input.sizeBytes))) : 0;
4117
+ const storageProvider = input.storageProvider === 'r2' || input.storageProvider === 'external' ? input.storageProvider : 'local';
4118
+ if (!draftId)
4119
+ throw new TaskforceRuleError('draftId is required', 400, 'AVATAR_DRAFT_ID_REQUIRED');
4120
+ if (!userId)
4121
+ throw new TaskforceRuleError('userId is required', 400, 'USER_ID_REQUIRED');
4122
+ if (!storageKey)
4123
+ throw new TaskforceRuleError('storageKey is required', 400, 'AVATAR_STORAGE_KEY_REQUIRED');
4124
+ const now = new Date().toISOString();
4125
+ this.db.prepare(`
4126
+ INSERT INTO user_avatar_upload_drafts (
4127
+ draft_id, tenant_id, user_id, storage_key, mime_type, original_filename, size_bytes, storage_provider,
4128
+ finalized_at, consumed_at, discarded_at, created_at, updated_at
4129
+ )
4130
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, NULL, NULL, NULL, ?, ?)
4131
+ `).run(draftId, this.tenantId, userId, storageKey, mimeType, originalFilename, sizeBytes, storageProvider, now, now);
4132
+ return this.getUserAvatarUploadDraft(userId, draftId);
4133
+ }
4134
+ getUserAvatarUploadDraft(userId, draftId) {
4135
+ const normalizedUserId = String(userId || '').trim();
4136
+ const normalizedDraftId = String(draftId || '').trim();
4137
+ if (!normalizedUserId || !normalizedDraftId)
4138
+ return null;
4139
+ const row = this.db.prepare(`
4140
+ SELECT draft_id, user_id, storage_key, mime_type, original_filename, size_bytes, storage_provider,
4141
+ finalized_at, consumed_at, discarded_at, created_at, updated_at
4142
+ FROM user_avatar_upload_drafts
4143
+ WHERE tenant_id = ? AND user_id = ? AND draft_id = ?
4144
+ LIMIT 1
4145
+ `).get(this.tenantId, normalizedUserId, normalizedDraftId);
4146
+ if (!row?.draft_id)
4147
+ return null;
4148
+ return {
4149
+ draftId: String(row.draft_id),
4150
+ userId: String(row.user_id || normalizedUserId),
4151
+ storageKey: String(row.storage_key || ''),
4152
+ mimeType: String(row.mime_type || 'application/octet-stream'),
4153
+ originalFilename: String(row.original_filename || 'avatar'),
4154
+ sizeBytes: Number(row.size_bytes || 0),
4155
+ storageProvider: String(row.storage_provider || 'local') === 'r2'
4156
+ ? 'r2'
4157
+ : String(row.storage_provider || 'local') === 'external'
4158
+ ? 'external'
4159
+ : 'local',
4160
+ finalizedAt: row.finalized_at ? String(row.finalized_at) : null,
4161
+ consumedAt: row.consumed_at ? String(row.consumed_at) : null,
4162
+ discardedAt: row.discarded_at ? String(row.discarded_at) : null,
4163
+ createdAt: String(row.created_at || ''),
4164
+ updatedAt: String(row.updated_at || '')
4165
+ };
4166
+ }
4167
+ markUserAvatarUploadDraftFinalized(userId, draftId) {
4168
+ const existing = this.getUserAvatarUploadDraft(userId, draftId);
4169
+ if (!existing || existing.discardedAt)
4170
+ return null;
4171
+ const now = new Date().toISOString();
4172
+ this.db.prepare(`
4173
+ UPDATE user_avatar_upload_drafts
4174
+ SET finalized_at = COALESCE(finalized_at, ?), updated_at = ?
4175
+ WHERE tenant_id = ? AND user_id = ? AND draft_id = ? AND discarded_at IS NULL
4176
+ `).run(now, now, this.tenantId, existing.userId, existing.draftId);
4177
+ return this.getUserAvatarUploadDraft(existing.userId, existing.draftId);
4178
+ }
4179
+ markUserAvatarUploadDraftConsumed(userId, draftId) {
4180
+ const existing = this.getUserAvatarUploadDraft(userId, draftId);
4181
+ if (!existing || existing.discardedAt)
4182
+ return null;
4183
+ const now = new Date().toISOString();
4184
+ this.db.prepare(`
4185
+ UPDATE user_avatar_upload_drafts
4186
+ SET consumed_at = COALESCE(consumed_at, ?), updated_at = ?
4187
+ WHERE tenant_id = ? AND user_id = ? AND draft_id = ? AND discarded_at IS NULL
4188
+ `).run(now, now, this.tenantId, existing.userId, existing.draftId);
4189
+ return this.getUserAvatarUploadDraft(existing.userId, existing.draftId);
4190
+ }
4191
+ discardUserAvatarUploadDraft(userId, draftId) {
4192
+ const existing = this.getUserAvatarUploadDraft(userId, draftId);
4193
+ if (!existing || existing.discardedAt)
4194
+ return existing;
4195
+ const now = new Date().toISOString();
4196
+ this.db.prepare(`
4197
+ UPDATE user_avatar_upload_drafts
4198
+ SET discarded_at = ?, updated_at = ?
4199
+ WHERE tenant_id = ? AND user_id = ? AND draft_id = ? AND discarded_at IS NULL
4200
+ `).run(now, now, this.tenantId, existing.userId, existing.draftId);
4201
+ return this.getUserAvatarUploadDraft(existing.userId, existing.draftId);
4202
+ }
4203
+ cleanupStaleUserAvatarUploadDrafts(userId, olderThanIso) {
4204
+ const normalizedUserId = String(userId || '').trim();
4205
+ const cutoff = String(olderThanIso || '').trim();
4206
+ if (!normalizedUserId || !cutoff)
4207
+ return [];
4208
+ const rows = this.db.prepare(`
4209
+ SELECT draft_id
4210
+ FROM user_avatar_upload_drafts
4211
+ WHERE tenant_id = ?
4212
+ AND user_id = ?
4213
+ AND discarded_at IS NULL
4214
+ AND consumed_at IS NULL
4215
+ AND created_at < ?
4216
+ `).all(this.tenantId, normalizedUserId, cutoff);
4217
+ const staleDrafts = rows
4218
+ .map((row) => this.getUserAvatarUploadDraft(normalizedUserId, String(row?.draft_id || '')))
4219
+ .filter((row) => Boolean(row && !row.discardedAt));
4220
+ if (staleDrafts.length === 0)
4221
+ return [];
4222
+ const now = new Date().toISOString();
4223
+ this.db.prepare(`
4224
+ UPDATE user_avatar_upload_drafts
4225
+ SET discarded_at = ?, updated_at = ?
4226
+ WHERE tenant_id = ? AND user_id = ? AND discarded_at IS NULL AND consumed_at IS NULL AND created_at < ?
4227
+ `).run(now, now, this.tenantId, normalizedUserId, cutoff);
4228
+ return staleDrafts;
4229
+ }
4098
4230
  getSystemRole(userId) {
4099
4231
  const normalizedUserId = String(userId || '').trim();
4100
4232
  if (!normalizedUserId || normalizedUserId === 'anonymous')
@@ -4245,6 +4377,193 @@ export class TaskforceCore {
4245
4377
  `).run(enabled ? 1 : 0, new Date().toISOString(), this.tenantId, normalizedUserId);
4246
4378
  return Number(result?.changes || 0) > 0;
4247
4379
  }
4380
+ purgeSystemUser(input) {
4381
+ const targetUserId = String(input.targetUserId || '').trim();
4382
+ const actorUserId = String(input.actorUserId || '').trim();
4383
+ const emailConfirmation = String(input.emailConfirmation || '').trim().toLowerCase();
4384
+ if (!targetUserId) {
4385
+ throw new TaskforceRuleError('targetUserId is required', 400, 'TARGET_USER_ID_REQUIRED');
4386
+ }
4387
+ if (!actorUserId || actorUserId === 'anonymous') {
4388
+ throw new TaskforceRuleError('actorUserId is required', 400, 'ACTOR_USER_ID_REQUIRED');
4389
+ }
4390
+ if (!emailConfirmation) {
4391
+ throw new TaskforceRuleError('Email confirmation is required', 400, 'EMAIL_CONFIRMATION_REQUIRED');
4392
+ }
4393
+ if (targetUserId === actorUserId) {
4394
+ throw new TaskforceRuleError('Cannot purge the current system admin session user', 400, 'CURRENT_SYSTEM_USER_PURGE_FORBIDDEN');
4395
+ }
4396
+ return this.db.transaction(() => {
4397
+ const user = this.db.prepare(`
4398
+ SELECT
4399
+ id,
4400
+ email,
4401
+ COALESCE(system_role, 'user') AS system_role,
4402
+ COALESCE(is_disabled, 0) AS is_disabled
4403
+ FROM users
4404
+ WHERE tenant_id = ? AND id = ?
4405
+ LIMIT 1
4406
+ `).get(this.tenantId, targetUserId);
4407
+ if (!user?.id || !user.email) {
4408
+ throw new TaskforceRuleError('User not found', 404, 'USER_NOT_FOUND');
4409
+ }
4410
+ const normalizedEmail = String(user.email).trim().toLowerCase();
4411
+ if (normalizedEmail !== emailConfirmation) {
4412
+ throw new TaskforceRuleError('Email confirmation did not match the target user', 400, 'EMAIL_CONFIRMATION_MISMATCH');
4413
+ }
4414
+ const currentRole = this.normalizeSystemRole(user.system_role, 'user');
4415
+ const isDisabled = Boolean(user.is_disabled);
4416
+ if (currentRole === 'system_admin' && !isDisabled && this.countSystemAdmins() <= 1) {
4417
+ throw new TaskforceRuleError('Cannot purge the last enabled system admin', 400, 'LAST_SYSTEM_ADMIN_REQUIRED');
4418
+ }
4419
+ const elevatedWorkspaceMemberships = this.db.prepare(`
4420
+ SELECT
4421
+ m.workspace_id AS workspace_id,
4422
+ COALESCE(m.role, 'member') AS role,
4423
+ w.name AS workspace_name
4424
+ FROM workspace_memberships m
4425
+ LEFT JOIN workspaces w
4426
+ ON w.tenant_id = m.tenant_id
4427
+ AND w.id = m.workspace_id
4428
+ WHERE m.tenant_id = ?
4429
+ AND m.user_id = ?
4430
+ AND COALESCE(m.status, 'active') = 'active'
4431
+ AND COALESCE(m.role, 'member') IN ('owner', 'admin')
4432
+ ORDER BY COALESCE(w.updated_at, w.created_at) DESC, w.name ASC
4433
+ LIMIT 10
4434
+ `).all(this.tenantId, targetUserId);
4435
+ if (elevatedWorkspaceMemberships.length > 0) {
4436
+ throw new TaskforceRuleError('Cannot purge a user with active owner/admin workspace memberships', 409, 'SYSTEM_USER_PURGE_BLOCKED_ACTIVE_WORKSPACE_ROLE', {
4437
+ memberships: elevatedWorkspaceMemberships.map((membership) => ({
4438
+ workspaceId: membership.workspace_id ? String(membership.workspace_id) : null,
4439
+ workspaceName: membership.workspace_name ? String(membership.workspace_name) : null,
4440
+ role: membership.role ? String(membership.role) : null
4441
+ }))
4442
+ });
4443
+ }
4444
+ const billingRow = this.db.prepare(`
4445
+ SELECT
4446
+ stripe_customer_id,
4447
+ stripe_subscription_id,
4448
+ stripe_price_id,
4449
+ stripe_status,
4450
+ billing_interval
4451
+ FROM user_billing
4452
+ WHERE tenant_id = ? AND user_id = ?
4453
+ LIMIT 1
4454
+ `).get(this.tenantId, targetUserId);
4455
+ const hasCommercialBillingState = [
4456
+ billingRow?.stripe_customer_id,
4457
+ billingRow?.stripe_subscription_id,
4458
+ billingRow?.stripe_price_id,
4459
+ billingRow?.stripe_status,
4460
+ billingRow?.billing_interval
4461
+ ].some((value) => String(value || '').trim().length > 0);
4462
+ if (hasCommercialBillingState) {
4463
+ throw new TaskforceRuleError('Cannot purge a user with active commercial billing state', 409, 'SYSTEM_USER_PURGE_BLOCKED_BILLING_STATE');
4464
+ }
4465
+ const now = new Date().toISOString();
4466
+ const runCount = (sql, ...params) => Number(this.db.prepare(sql).run(...params)?.changes || 0);
4467
+ const unassignedTaskCount = runCount(`
4468
+ UPDATE tasks
4469
+ SET assignee = 'unassigned',
4470
+ updated_at = ?
4471
+ WHERE tenant_id = ?
4472
+ AND assignee = ?
4473
+ AND is_archived = 0
4474
+ `, now, this.tenantId, targetUserId);
4475
+ const clearedInitiativeOwnerCount = runCount(`
4476
+ UPDATE initiatives
4477
+ SET owner_id = NULL,
4478
+ updated_at = ?
4479
+ WHERE tenant_id = ?
4480
+ AND owner_id = ?
4481
+ `, now, this.tenantId, targetUserId);
4482
+ const clearedWorkstreamOwnerCount = runCount(`
4483
+ UPDATE workstreams
4484
+ SET owner_id = NULL,
4485
+ updated_at = ?
4486
+ WHERE tenant_id = ?
4487
+ AND owner_id = ?
4488
+ `, now, this.tenantId, targetUserId);
4489
+ const deletedEntitlementCount = runCount(`
4490
+ DELETE FROM account_entitlements
4491
+ WHERE tenant_id = ?
4492
+ AND account_id = ?
4493
+ `, this.tenantId, targetUserId);
4494
+ const deletedWorkspaceMembershipCount = runCount(`
4495
+ DELETE FROM workspace_memberships
4496
+ WHERE tenant_id = ?
4497
+ AND user_id = ?
4498
+ `, this.tenantId, targetUserId);
4499
+ const deletedAuthTokenCount = runCount(`
4500
+ DELETE FROM auth_tokens
4501
+ WHERE tenant_id = ?
4502
+ AND user_id = ?
4503
+ `, this.tenantId, targetUserId);
4504
+ const deletedAvatarDraftCount = runCount(`
4505
+ DELETE FROM user_avatar_upload_drafts
4506
+ WHERE tenant_id = ?
4507
+ AND user_id = ?
4508
+ `, this.tenantId, targetUserId);
4509
+ const deletedMcpAccessTokenCount = runCount(`
4510
+ DELETE FROM mcp_access_tokens
4511
+ WHERE tenant_id = ?
4512
+ AND user_id = ?
4513
+ `, this.tenantId, targetUserId);
4514
+ const deletedMcpOauthAuthCodeCount = runCount(`
4515
+ DELETE FROM mcp_oauth_auth_codes
4516
+ WHERE tenant_id = ?
4517
+ AND user_id = ?
4518
+ `, this.tenantId, targetUserId);
4519
+ const deletedMcpOauthConnectorGrantCount = runCount(`
4520
+ DELETE FROM mcp_oauth_connector_grants
4521
+ WHERE tenant_id = ?
4522
+ AND user_id = ?
4523
+ `, this.tenantId, targetUserId);
4524
+ const deletedUiStateCount = runCount(`
4525
+ DELETE FROM ui_state
4526
+ WHERE tenant_id = ?
4527
+ AND user_id = ?
4528
+ `, this.tenantId, targetUserId);
4529
+ const deletedUserPreferenceCount = runCount(`
4530
+ DELETE FROM user_preferences
4531
+ WHERE tenant_id = ?
4532
+ AND user_id = ?
4533
+ `, this.tenantId, targetUserId);
4534
+ const deletedUserBillingCount = runCount(`
4535
+ DELETE FROM user_billing
4536
+ WHERE tenant_id = ?
4537
+ AND user_id = ?
4538
+ `, this.tenantId, targetUserId);
4539
+ const deletedUserCount = runCount(`
4540
+ DELETE FROM users
4541
+ WHERE tenant_id = ?
4542
+ AND id = ?
4543
+ `, this.tenantId, targetUserId);
4544
+ if (deletedUserCount !== 1) {
4545
+ throw new TaskforceRuleError('User not found', 404, 'USER_NOT_FOUND');
4546
+ }
4547
+ return {
4548
+ targetUserId,
4549
+ email: String(user.email),
4550
+ unassignedTaskCount,
4551
+ clearedInitiativeOwnerCount,
4552
+ clearedWorkstreamOwnerCount,
4553
+ deletedWorkspaceMembershipCount,
4554
+ deletedAuthTokenCount,
4555
+ deletedAvatarDraftCount,
4556
+ deletedMcpAccessTokenCount,
4557
+ deletedMcpOauthAuthCodeCount,
4558
+ deletedMcpOauthConnectorGrantCount,
4559
+ deletedUiStateCount,
4560
+ deletedUserPreferenceCount,
4561
+ deletedEntitlementCount,
4562
+ deletedUserBillingCount,
4563
+ deletedUserCount
4564
+ };
4565
+ })();
4566
+ }
4248
4567
  clearActiveAssignmentsForUnavailableActor(actorId, workspaceId, updatedAt) {
4249
4568
  const normalizedActorId = String(actorId || '').trim();
4250
4569
  const normalizedWorkspaceId = String(workspaceId || 'default').trim() || 'default';
@@ -5199,7 +5518,20 @@ export class TaskforceCore {
5199
5518
  const assignFirstSystemAdmin = !this.hasAnySystemAdmin();
5200
5519
  const betaAccess = input.betaAccess !== false;
5201
5520
  const requestedWorkspaceId = String(input.workspaceId || '').trim();
5202
- const registrationPlanAssignment = this.resolveRegistrationPlanAssignment(input.planVersionId);
5521
+ const onboardingOutcome = this.resolveRegistrationOnboardingOutcome(input.planVersionId);
5522
+ const applyRegistrationCommercialState = (accountId, now) => {
5523
+ this.transitionCanonicalCommercialState({
5524
+ accountId,
5525
+ planVersionId: onboardingOutcome.planVersionId,
5526
+ state: onboardingOutcome.commercialState,
5527
+ effectiveAt: now,
5528
+ billingMirrorPlanId: onboardingOutcome.planId,
5529
+ source: 'registration',
5530
+ updatedBy: 'registration',
5531
+ createdAt: now,
5532
+ updatedAt: now
5533
+ });
5534
+ };
5203
5535
  if (!requestedWorkspaceId || requestedWorkspaceId.toLowerCase() === 'default') {
5204
5536
  const now = new Date().toISOString();
5205
5537
  const userId = this.generateEntityId('user');
@@ -5208,32 +5540,14 @@ export class TaskforceCore {
5208
5540
  INSERT INTO users (id, tenant_id, email, display_name, auth_provider, external_auth_id, password_hash, system_role, beta_access, is_disabled, created_at, updated_at)
5209
5541
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 0, ?, ?)
5210
5542
  `).run(userId, this.tenantId, email, displayName, 'password', email, hashPassword(password), assignFirstSystemAdmin ? 'system_admin' : 'user', betaAccess ? 1 : 0, now, now);
5211
- if (registrationPlanAssignment.requiresPlanSelection) {
5212
- this.createUserBillingRecord(userId, {
5213
- entitlementState: 'pending_plan_selection',
5214
- planId: null
5215
- });
5216
- }
5217
- if (!registrationPlanAssignment.requiresPlanSelection && registrationPlanAssignment.planVersionId && registrationPlanAssignment.planId) {
5218
- this.createUserBillingRecord(userId, {
5219
- entitlementState: 'active',
5220
- planId: registrationPlanAssignment.planId
5221
- });
5222
- this.upsertCanonicalEntitlementRecord({
5223
- accountId: userId,
5224
- planVersionId: registrationPlanAssignment.planVersionId,
5225
- state: 'active',
5226
- effectiveAt: now,
5227
- updatedBy: 'registration',
5228
- createdAt: now,
5229
- updatedAt: now
5230
- });
5231
- }
5543
+ applyRegistrationCommercialState(userId, now);
5232
5544
  return {
5233
5545
  userId,
5234
5546
  workspaceId: null,
5235
5547
  role: 'member',
5236
- planSelectionRequired: registrationPlanAssignment.requiresPlanSelection
5548
+ planSelectionRequired: onboardingOutcome.requiresPlanSelection,
5549
+ checkoutPending: onboardingOutcome.checkoutPending,
5550
+ commercialState: onboardingOutcome.commercialState
5237
5551
  };
5238
5552
  }
5239
5553
  const workspaceId = requestedWorkspaceId;
@@ -5251,27 +5565,12 @@ export class TaskforceCore {
5251
5565
  WHERE tenant_id = ? AND id = ?
5252
5566
  `).run(new Date().toISOString(), this.tenantId, created.userId);
5253
5567
  }
5254
- if (registrationPlanAssignment.requiresPlanSelection) {
5255
- this.createUserBillingRecord(created.userId, {
5256
- entitlementState: 'pending_plan_selection',
5257
- planId: null
5258
- });
5259
- }
5260
- if (!registrationPlanAssignment.requiresPlanSelection && registrationPlanAssignment.planVersionId && registrationPlanAssignment.planId) {
5261
- this.createUserBillingRecord(created.userId, {
5262
- entitlementState: 'active',
5263
- planId: registrationPlanAssignment.planId
5264
- });
5265
- this.upsertCanonicalEntitlementRecord({
5266
- accountId: created.userId,
5267
- planVersionId: registrationPlanAssignment.planVersionId,
5268
- state: 'active',
5269
- updatedBy: 'registration'
5270
- });
5271
- }
5568
+ applyRegistrationCommercialState(created.userId);
5272
5569
  return {
5273
5570
  ...created,
5274
- planSelectionRequired: registrationPlanAssignment.requiresPlanSelection
5571
+ planSelectionRequired: onboardingOutcome.requiresPlanSelection,
5572
+ checkoutPending: onboardingOutcome.checkoutPending,
5573
+ commercialState: onboardingOutcome.commercialState
5275
5574
  };
5276
5575
  }
5277
5576
  createOneTimeAuthToken(userId, purpose, ttlSeconds, workspaceId) {
@@ -5770,6 +6069,9 @@ export class TaskforceCore {
5770
6069
  this.assertRequiredTaskTaxonomies(normalizedInput.taxonomies, normalizedWorkspaceId);
5771
6070
  const incomingId = String(normalizedInput.id || '').trim();
5772
6071
  const incomingReferenceNumber = this.sanitizeTaskReferenceNumber(normalizedInput.referenceNumber);
6072
+ const useProvisionalLocalReference = options?.provisionalLocalReference === true
6073
+ && options?.allowClientProvidedId !== true
6074
+ && !incomingReferenceNumber;
5773
6075
  if (incomingId && !allowClientProvidedId) {
5774
6076
  throw new TaskforceRuleError('Client-supplied task id is not allowed for this operation.', 400, 'TASK_ID_NOT_ALLOWED');
5775
6077
  }
@@ -5777,7 +6079,15 @@ export class TaskforceCore {
5777
6079
  throw new TaskforceRuleError('Client-supplied task reference number is not allowed for this operation.', 400, 'TASK_REFERENCE_NOT_ALLOWED');
5778
6080
  }
5779
6081
  const id = incomingId || this.generateId();
5780
- const referenceNumber = this.reserveTaskReferenceNumber(normalizedWorkspaceId, incomingReferenceNumber);
6082
+ if (incomingReferenceNumber && options?.preferIncomingReferenceNumberOnConflict === true) {
6083
+ this.relocateTaskReferenceConflict(normalizedWorkspaceId, incomingReferenceNumber, id, options?.syncUpdatedAt);
6084
+ }
6085
+ const referenceNumber = useProvisionalLocalReference
6086
+ ? null
6087
+ : this.reserveTaskReferenceNumber(normalizedWorkspaceId, incomingReferenceNumber, id);
6088
+ const localReferenceNumber = useProvisionalLocalReference
6089
+ ? this.allocateNextLocalTaskReferenceNumber(normalizedWorkspaceId, id)
6090
+ : null;
5781
6091
  const normalizedCategory = normalizedInput.category || TaskforceCore.DEFAULT_CATEGORY_VALUE;
5782
6092
  const normalizedType = normalizedInput.type || this.resolveDefaultTypeValue(normalizedWorkspaceId);
5783
6093
  const createdByInput = String(normalizedInput.createdBy || '').trim() || undefined;
@@ -5816,7 +6126,8 @@ export class TaskforceCore {
5816
6126
  const task = {
5817
6127
  id,
5818
6128
  referenceNumber,
5819
- referenceLabel: formatTaskReference(referenceNumber),
6129
+ localReferenceNumber,
6130
+ referenceLabel: deriveTaskReferenceLabel({ referenceNumber, localReferenceNumber }),
5820
6131
  title: normalizedInput.title || 'Untitled Task',
5821
6132
  description: normalizedDescription || null,
5822
6133
  category: normalizedCategory,
@@ -5863,6 +6174,7 @@ export class TaskforceCore {
5863
6174
  id, tenant_id, title, description, status, priority, complexity, type, category,
5864
6175
  workspace_id,
5865
6176
  reference_number,
6177
+ local_reference_number,
5866
6178
  approach, created_at, updated_at, is_archived, taxonomies, created_by, assignee,
5867
6179
  scheduled_date, due_date, scheduled_week_key, order_in_day,
5868
6180
  workstream_id
@@ -5870,6 +6182,7 @@ export class TaskforceCore {
5870
6182
  @id, @tenantId, @title, @description, @status, @priority, @complexity, @type, @category,
5871
6183
  @workspaceId,
5872
6184
  @referenceNumber,
6185
+ @localReferenceNumber,
5873
6186
  @approach, @createdAt, @updatedAt, 0, @taxonomies, @createdBy, @assignee,
5874
6187
  @scheduledDate, @dueDate, @scheduledWeekKey, @orderInDay,
5875
6188
  @workstreamId
@@ -5946,7 +6259,8 @@ export class TaskforceCore {
5946
6259
  created.push(this.createTask(batch[i], {
5947
6260
  persistBackup: false,
5948
6261
  workspaceId: options?.workspaceId,
5949
- allowClientProvidedId: options?.allowClientProvidedId
6262
+ allowClientProvidedId: options?.allowClientProvidedId,
6263
+ provisionalLocalReference: options?.provisionalLocalReference
5950
6264
  }));
5951
6265
  }
5952
6266
  if (validateOnly) {
@@ -5973,7 +6287,8 @@ export class TaskforceCore {
5973
6287
  this.createTask(draft, {
5974
6288
  persistBackup: false,
5975
6289
  workspaceId: options?.workspaceId,
5976
- allowClientProvidedId: options?.allowClientProvidedId
6290
+ allowClientProvidedId: options?.allowClientProvidedId,
6291
+ provisionalLocalReference: options?.provisionalLocalReference
5977
6292
  });
5978
6293
  throw validateOnlyRollbackError;
5979
6294
  })(input);
@@ -5982,7 +6297,8 @@ export class TaskforceCore {
5982
6297
  created.push(this.createTask(input, {
5983
6298
  persistBackup: false,
5984
6299
  workspaceId: options?.workspaceId,
5985
- allowClientProvidedId: options?.allowClientProvidedId
6300
+ allowClientProvidedId: options?.allowClientProvidedId,
6301
+ provisionalLocalReference: options?.provisionalLocalReference
5986
6302
  }));
5987
6303
  }
5988
6304
  }
@@ -6075,7 +6391,19 @@ export class TaskforceCore {
6075
6391
  }
6076
6392
  }
6077
6393
  if (Object.prototype.hasOwnProperty.call(patch, 'referenceNumber')) {
6078
- patch.referenceNumber = this.sanitizeTaskReferenceNumber(patch.referenceNumber);
6394
+ const requestedReferenceNumber = this.sanitizeTaskReferenceNumber(patch.referenceNumber);
6395
+ if (requestedReferenceNumber && options?.preferIncomingReferenceNumberOnConflict === true) {
6396
+ this.relocateTaskReferenceConflict(normalizedWorkspaceId, requestedReferenceNumber, taskId, options?.syncUpdatedAt);
6397
+ }
6398
+ patch.referenceNumber = requestedReferenceNumber
6399
+ ? this.reserveTaskReferenceNumber(normalizedWorkspaceId, requestedReferenceNumber, taskId)
6400
+ : requestedReferenceNumber;
6401
+ if (requestedReferenceNumber) {
6402
+ patch.localReferenceNumber = null;
6403
+ }
6404
+ }
6405
+ if (Object.prototype.hasOwnProperty.call(patch, 'localReferenceNumber')) {
6406
+ patch.localReferenceNumber = this.sanitizeTaskReferenceNumber(patch.localReferenceNumber);
6079
6407
  }
6080
6408
  if (patch.status !== undefined) {
6081
6409
  patch.status = this.normalizeStatusValue(patch.status);
@@ -6093,7 +6421,10 @@ export class TaskforceCore {
6093
6421
  const updatedTask = {
6094
6422
  ...existing,
6095
6423
  ...patch,
6096
- referenceLabel: formatTaskReference(patch.referenceNumber ?? existing.referenceNumber),
6424
+ referenceLabel: deriveTaskReferenceLabel({
6425
+ referenceNumber: patch.referenceNumber ?? existing.referenceNumber,
6426
+ localReferenceNumber: patch.localReferenceNumber ?? existing.localReferenceNumber
6427
+ }),
6097
6428
  updatedAt: options?.syncUpdatedAt || patch.updatedAt || new Date().toISOString()
6098
6429
  };
6099
6430
  const fields = [];
@@ -6101,13 +6432,14 @@ export class TaskforceCore {
6101
6432
  const mappableFields = [
6102
6433
  'title', 'description', 'status', 'completedAt', 'priority', 'complexity', 'type', 'category',
6103
6434
  'approach', 'canceledReason', 'createdBy', 'assignee', 'scheduledDate', 'dueDate',
6104
- 'scheduledWeekKey', 'orderInDay', 'workstreamId', 'referenceNumber'
6435
+ 'scheduledWeekKey', 'orderInDay', 'workstreamId', 'referenceNumber', 'localReferenceNumber'
6105
6436
  ];
6106
6437
  const propertyToColumn = {
6107
6438
  canceledReason: 'canceled_reason',
6108
6439
  completedAt: 'completed_at',
6109
6440
  createdBy: 'created_by',
6110
6441
  referenceNumber: 'reference_number',
6442
+ localReferenceNumber: 'local_reference_number',
6111
6443
  scheduledDate: 'scheduled_date',
6112
6444
  dueDate: 'due_date',
6113
6445
  scheduledWeekKey: 'scheduled_week_key',