@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
@@ -1,5 +1,5 @@
1
1
  import Stripe from 'stripe';
2
- import { parseJsonBody } from '../routes.js';
2
+ import { parseJsonBody } from './primitives.js';
3
3
  import { resolveTaskforceCloudEnvironmentFromHostname } from '../../config/cloudEnvironment.js';
4
4
  import { buildMockStripeHostedCheckoutUrl, completeMockStripeCheckoutSession, createMockStripeClient, isMockStripeClient, isMockStripeEnabled, readMockStripeCheckoutSession, renderMockStripeCheckoutHtml } from '../mockStripe.js';
5
5
  const BILLING_CONFIG_KEY_PLAN = 'billing.config.v1';
@@ -795,11 +795,15 @@ function applySubscriptionSnapshot(core, input) {
795
795
  ? (currentPeriodEnd && currentPeriodEnd > nowIso ? 'active' : 'canceled')
796
796
  : null;
797
797
  const mappedCanonicalState = canceledState || mapStripeStatusToCanonicalState(stripeStatus);
798
+ const existingPlanVersionId = String(existingEntitlement?.plan_version_id || '').trim();
798
799
  const preserveExistingAccess = mappedCanonicalState === 'checkout_pending'
800
+ && Boolean(planVersionId)
801
+ && existingPlanVersionId === planVersionId
799
802
  && isCommercialStateAppAccessible(existingEntitlement?.entitlement_state);
800
803
  const canonicalState = preserveExistingAccess
801
804
  ? String(existingEntitlement?.entitlement_state || 'active').trim().toLowerCase()
802
805
  : mappedCanonicalState;
806
+ const billingConfirmedSource = source === 'stripe-webhook' || source === 'checkout-confirm';
803
807
  const effectiveUntil = stripeStatus === 'canceled'
804
808
  ? currentPeriodEnd
805
809
  : (currentPeriodEnd || null);
@@ -826,7 +830,7 @@ function applySubscriptionSnapshot(core, input) {
826
830
  });
827
831
  }
828
832
  }
829
- else if (planVersionId) {
833
+ else if (planVersionId && billingConfirmedSource) {
830
834
  core.transitionCanonicalCommercialState({
831
835
  accountId: userId,
832
836
  planVersionId: planVersionId || existingEntitlement?.plan_version_id || core.getDefaultPlanVersionIdByMode('personal', { allowFallback: true }),
@@ -1326,18 +1330,11 @@ export function registerBillingRoutes(deps) {
1326
1330
  res.end(JSON.stringify({ error: 'Forbidden: system admin role required.' }));
1327
1331
  return;
1328
1332
  }
1329
- try {
1330
- const requestUrl = parseRequestUrl(req);
1331
- const interval = parseInterval(requestUrl.searchParams.get('interval'));
1332
- const options = await listStripePriceOptions(interval);
1333
- res.writeHead(200, { 'Content-Type': 'application/json' });
1334
- res.end(JSON.stringify({ options }));
1335
- }
1336
- catch (error) {
1337
- const statusCode = resolveErrorStatusCode(error, 400);
1338
- res.writeHead(statusCode, { 'Content-Type': 'application/json' });
1339
- res.end(JSON.stringify({ error: String(error?.message || 'Unable to list Stripe prices'), code: error?.code }));
1340
- }
1333
+ res.writeHead(410, { 'Content-Type': 'application/json' });
1334
+ res.end(JSON.stringify({
1335
+ error: 'Manual Stripe price selection has been removed. Refresh prices from Stripe metadata instead.',
1336
+ code: 'BILLING_PRICE_MANUAL_MAPPING_REMOVED'
1337
+ }));
1341
1338
  });
1342
1339
  addRoute('PUT', '/api/taskforce/admin/billing/price-mappings', async (req, res) => {
1343
1340
  if (!ensureCloudBilling(req, res))
@@ -1347,121 +1344,11 @@ export function registerBillingRoutes(deps) {
1347
1344
  res.end(JSON.stringify({ error: 'Forbidden: system admin role required.' }));
1348
1345
  return;
1349
1346
  }
1350
- const body = await parseJsonBody(req);
1351
- const planVersionId = String(body?.planVersionId || '').trim();
1352
- const interval = parseInterval(body?.interval);
1353
- const pricingType = parsePricingType(body?.pricingType) || 'stripe';
1354
- const stripePriceId = String(body?.stripePriceId || '').trim();
1355
- const setActive = body?.active === false ? false : true;
1356
- if (!planVersionId || !interval) {
1357
- res.writeHead(400, { 'Content-Type': 'application/json' });
1358
- res.end(JSON.stringify({ error: 'planVersionId and interval are required.', code: 'BILLING_MAPPING_INPUT_REQUIRED' }));
1359
- return;
1360
- }
1361
- if (pricingType === 'stripe' && !stripePriceId) {
1362
- res.writeHead(400, { 'Content-Type': 'application/json' });
1363
- res.end(JSON.stringify({ error: 'stripePriceId is required for Stripe pricing.', code: 'BILLING_MAPPING_STRIPE_PRICE_REQUIRED' }));
1364
- return;
1365
- }
1366
- const db = core.getDatabaseAdapter();
1367
- const tenantId = core.getTenantId();
1368
- const versionExists = db.prepare(`
1369
- SELECT plan_version_id
1370
- FROM plan_versions
1371
- WHERE tenant_id = ? AND plan_version_id = ?
1372
- LIMIT 1
1373
- `).get(tenantId, planVersionId);
1374
- if (!versionExists?.plan_version_id) {
1375
- res.writeHead(404, { 'Content-Type': 'application/json' });
1376
- res.end(JSON.stringify({ error: 'Plan version not found.', code: 'PLAN_VERSION_NOT_FOUND' }));
1377
- return;
1378
- }
1379
- try {
1380
- const now = new Date().toISOString();
1381
- const targetStripePriceId = pricingType === 'free'
1382
- ? buildFreePricingSyntheticId(planVersionId, interval)
1383
- : stripePriceId;
1384
- let unitAmount = 0;
1385
- let currency = null;
1386
- if (pricingType === 'stripe') {
1387
- const stripe = getStripeClient();
1388
- const stripePrice = await stripe.prices.retrieve(stripePriceId);
1389
- const stripeInterval = parseInterval(stripePrice.recurring?.interval || '');
1390
- if (!stripePrice.active) {
1391
- res.writeHead(422, { 'Content-Type': 'application/json' });
1392
- res.end(JSON.stringify({ error: 'Stripe price is not active.', code: 'STRIPE_PRICE_INACTIVE' }));
1393
- return;
1394
- }
1395
- if (!stripeInterval) {
1396
- res.writeHead(422, { 'Content-Type': 'application/json' });
1397
- res.end(JSON.stringify({ error: 'Stripe price must be recurring month/year.', code: 'STRIPE_PRICE_INTERVAL_UNSUPPORTED' }));
1398
- return;
1399
- }
1400
- if (stripeInterval !== interval) {
1401
- res.writeHead(422, { 'Content-Type': 'application/json' });
1402
- res.end(JSON.stringify({ error: `Stripe price interval (${stripeInterval}) does not match requested interval (${interval}).`, code: 'STRIPE_PRICE_INTERVAL_MISMATCH' }));
1403
- return;
1404
- }
1405
- unitAmount = typeof stripePrice.unit_amount === 'number' ? stripePrice.unit_amount : null;
1406
- currency = stripePrice.currency || null;
1407
- }
1408
- const beforeRows = db.prepare(`
1409
- SELECT plan_version_id, billing_interval, stripe_price_id, pricing_type, active, unit_amount, currency
1410
- FROM billing_price_mappings
1411
- WHERE tenant_id = ? AND plan_version_id = ? AND billing_interval = ?
1412
- ORDER BY stripe_price_id ASC
1413
- `).all(tenantId, planVersionId, interval);
1414
- if (setActive) {
1415
- db.prepare(`
1416
- UPDATE billing_price_mappings
1417
- SET active = 0, updated_at = ?
1418
- WHERE tenant_id = ?
1419
- AND plan_version_id = ?
1420
- AND billing_interval = ?
1421
- AND stripe_price_id <> ?
1422
- AND active = 1
1423
- `).run(now, tenantId, planVersionId, interval, targetStripePriceId);
1424
- }
1425
- db.prepare(`
1426
- INSERT INTO billing_price_mappings (
1427
- tenant_id, plan_version_id, billing_interval, stripe_price_id, pricing_type, active, unit_amount, currency, created_at, updated_at
1428
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1429
- ON CONFLICT (tenant_id, plan_version_id, billing_interval, stripe_price_id) DO UPDATE SET
1430
- pricing_type = excluded.pricing_type,
1431
- active = excluded.active,
1432
- unit_amount = excluded.unit_amount,
1433
- currency = excluded.currency,
1434
- updated_at = excluded.updated_at
1435
- `).run(tenantId, planVersionId, interval, targetStripePriceId, pricingType, setActive ? 1 : 0, unitAmount, currency, now, now);
1436
- const mapping = db.prepare(`
1437
- SELECT plan_version_id, billing_interval, stripe_price_id, pricing_type, active, unit_amount, currency, created_at, updated_at
1438
- FROM billing_price_mappings
1439
- WHERE tenant_id = ? AND plan_version_id = ? AND billing_interval = ? AND stripe_price_id = ?
1440
- LIMIT 1
1441
- `).get(tenantId, planVersionId, interval, targetStripePriceId);
1442
- const afterRows = db.prepare(`
1443
- SELECT plan_version_id, billing_interval, stripe_price_id, pricing_type, active, unit_amount, currency
1444
- FROM billing_price_mappings
1445
- WHERE tenant_id = ? AND plan_version_id = ? AND billing_interval = ?
1446
- ORDER BY stripe_price_id ASC
1447
- `).all(tenantId, planVersionId, interval);
1448
- auditAdminAction(req, 'billing-price-mapping-upsert', {
1449
- planVersionId,
1450
- interval,
1451
- pricingType,
1452
- stripePriceId: pricingType === 'free' ? null : stripePriceId,
1453
- active: setActive,
1454
- before: beforeRows,
1455
- after: afterRows
1456
- });
1457
- res.writeHead(200, { 'Content-Type': 'application/json' });
1458
- res.end(JSON.stringify({ mapping }));
1459
- }
1460
- catch (error) {
1461
- const statusCode = resolveErrorStatusCode(error, 400);
1462
- res.writeHead(statusCode, { 'Content-Type': 'application/json' });
1463
- res.end(JSON.stringify({ error: String(error?.message || 'Unable to upsert price mapping'), code: error?.code }));
1464
- }
1347
+ res.writeHead(410, { 'Content-Type': 'application/json' });
1348
+ res.end(JSON.stringify({
1349
+ error: 'Manual billing price mappings have been removed. Import Stripe prices instead.',
1350
+ code: 'BILLING_PRICE_MANUAL_MAPPING_REMOVED'
1351
+ }));
1465
1352
  });
1466
1353
  addRoute('POST', '/api/taskforce/admin/billing/price-mappings/deactivate', async (req, res) => {
1467
1354
  if (!ensureCloudBilling(req, res))
@@ -1471,51 +1358,11 @@ export function registerBillingRoutes(deps) {
1471
1358
  res.end(JSON.stringify({ error: 'Forbidden: system admin role required.' }));
1472
1359
  return;
1473
1360
  }
1474
- const body = await parseJsonBody(req);
1475
- const planVersionId = String(body?.planVersionId || '').trim();
1476
- const interval = parseInterval(body?.interval);
1477
- const stripePriceId = String(body?.stripePriceId || '').trim();
1478
- if (!planVersionId || !interval) {
1479
- res.writeHead(400, { 'Content-Type': 'application/json' });
1480
- res.end(JSON.stringify({ error: 'planVersionId and interval are required.', code: 'BILLING_MAPPING_TARGET_REQUIRED' }));
1481
- return;
1482
- }
1483
- const db = core.getDatabaseAdapter();
1484
- const tenantId = core.getTenantId();
1485
- const now = new Date().toISOString();
1486
- const beforeRows = db.prepare(`
1487
- SELECT plan_version_id, billing_interval, stripe_price_id, pricing_type, active, unit_amount, currency
1488
- FROM billing_price_mappings
1489
- WHERE tenant_id = ? AND plan_version_id = ? AND billing_interval = ?
1490
- ORDER BY stripe_price_id ASC
1491
- `).all(tenantId, planVersionId, interval);
1492
- const result = stripePriceId
1493
- ? db.prepare(`
1494
- UPDATE billing_price_mappings
1495
- SET active = 0, updated_at = ?
1496
- WHERE tenant_id = ? AND plan_version_id = ? AND billing_interval = ? AND stripe_price_id = ?
1497
- `).run(now, tenantId, planVersionId, interval, stripePriceId)
1498
- : db.prepare(`
1499
- UPDATE billing_price_mappings
1500
- SET active = 0, updated_at = ?
1501
- WHERE tenant_id = ? AND plan_version_id = ? AND billing_interval = ?
1502
- `).run(now, tenantId, planVersionId, interval);
1503
- const afterRows = db.prepare(`
1504
- SELECT plan_version_id, billing_interval, stripe_price_id, pricing_type, active, unit_amount, currency
1505
- FROM billing_price_mappings
1506
- WHERE tenant_id = ? AND plan_version_id = ? AND billing_interval = ?
1507
- ORDER BY stripe_price_id ASC
1508
- `).all(tenantId, planVersionId, interval);
1509
- auditAdminAction(req, 'billing-price-mapping-deactivate', {
1510
- planVersionId,
1511
- interval,
1512
- stripePriceId: stripePriceId || null,
1513
- changedRows: Number(result?.changes || 0),
1514
- before: beforeRows,
1515
- after: afterRows
1516
- });
1517
- res.writeHead(200, { 'Content-Type': 'application/json' });
1518
- res.end(JSON.stringify({ success: true, changedRows: Number(result?.changes || 0) }));
1361
+ res.writeHead(410, { 'Content-Type': 'application/json' });
1362
+ res.end(JSON.stringify({
1363
+ error: 'Manual billing price mappings have been removed. Refresh Stripe prices instead.',
1364
+ code: 'BILLING_PRICE_MANUAL_MAPPING_REMOVED'
1365
+ }));
1519
1366
  });
1520
1367
  addRoute('GET', '/api/taskforce/admin/billing/webhook-failures', async (req, res) => {
1521
1368
  if (!ensureCloudBilling(req, res))
@@ -1769,8 +1616,8 @@ export function registerBillingRoutes(deps) {
1769
1616
  const stripe = getStripeClient();
1770
1617
  let currentSubscriptionId = String(currentBilling?.stripe_subscription_id || '').trim();
1771
1618
  const currentCustomerId = String(currentBilling?.stripe_customer_id || '').trim();
1772
- const currentPlanVersionId = String(currentEntitlement?.plan_version_id || '').trim();
1773
- const currentInterval = parseInterval(String(currentBilling?.billing_interval || '').trim());
1619
+ let currentPlanVersionId = String(currentEntitlement?.plan_version_id || '').trim();
1620
+ let currentInterval = parseInterval(String(currentBilling?.billing_interval || '').trim());
1774
1621
  const stripeAccountDiscovery = await discoverStripeAccountStateForUser(core, {
1775
1622
  userId,
1776
1623
  normalizedEmail: normalizedUserEmail,
@@ -1786,8 +1633,13 @@ export function registerBillingRoutes(deps) {
1786
1633
  return;
1787
1634
  }
1788
1635
  let resolvedCustomerId = currentCustomerId || String(stripeAccountDiscovery.preferredCustomerId || '').trim();
1789
- if (!currentSubscriptionId && stripeAccountDiscovery.activeSubscriptions.length === 1) {
1790
- const activeSubscription = stripeAccountDiscovery.activeSubscriptions[0];
1636
+ const activeSubscription = stripeAccountDiscovery.activeSubscriptions.length === 1
1637
+ ? stripeAccountDiscovery.activeSubscriptions[0]
1638
+ : null;
1639
+ let discoveredPlanVersionId = null;
1640
+ let discoveredInterval = null;
1641
+ let discoveredStripePriceId = null;
1642
+ if (!currentSubscriptionId && activeSubscription) {
1791
1643
  currentSubscriptionId = String(activeSubscription.subscription?.id || '').trim();
1792
1644
  resolvedCustomerId = String(activeSubscription.customerId || '').trim() || resolvedCustomerId;
1793
1645
  if (currentSubscriptionId || resolvedCustomerId) {
@@ -1804,7 +1656,28 @@ export function registerBillingRoutes(deps) {
1804
1656
  stripeCustomerId: resolvedCustomerId
1805
1657
  });
1806
1658
  }
1807
- if (currentSubscriptionId && currentPlanVersionId === planVersionId && currentInterval === interval) {
1659
+ if (activeSubscription) {
1660
+ const activeSubscriptionPriceId = String(activeSubscription.subscription?.items?.data?.[0]?.price?.id || '').trim();
1661
+ discoveredStripePriceId = activeSubscriptionPriceId || null;
1662
+ const activePlanVersionId = resolvePlanVersionIdForBillingEvent(core, [
1663
+ activeSubscription.subscription?.metadata?.planVersionId,
1664
+ activeSubscription.subscription?.metadata?.plan_version_id
1665
+ ], activeSubscriptionPriceId).planVersionId;
1666
+ discoveredPlanVersionId = activePlanVersionId;
1667
+ discoveredInterval = parseInterval(activeSubscription.subscription?.items?.data?.[0]?.price?.recurring?.interval || '');
1668
+ if (!currentPlanVersionId && activePlanVersionId) {
1669
+ currentPlanVersionId = activePlanVersionId;
1670
+ }
1671
+ if (!currentInterval) {
1672
+ currentInterval = discoveredInterval;
1673
+ }
1674
+ }
1675
+ const isCurrentRequestedPlan = (currentPlanVersionId === planVersionId && currentInterval === interval) || (Boolean(currentSubscriptionId)
1676
+ && discoveredPlanVersionId === planVersionId
1677
+ && discoveredInterval === interval) || (Boolean(currentSubscriptionId)
1678
+ && discoveredStripePriceId === priceId
1679
+ && discoveredInterval === interval);
1680
+ if (currentSubscriptionId && isCurrentRequestedPlan) {
1808
1681
  res.writeHead(200, { 'Content-Type': 'application/json' });
1809
1682
  res.end(JSON.stringify({
1810
1683
  checkoutState: 'already_current_plan',
@@ -1816,6 +1689,17 @@ export function registerBillingRoutes(deps) {
1816
1689
  if (currentSubscriptionId) {
1817
1690
  const current = await stripe.subscriptions.retrieve(currentSubscriptionId);
1818
1691
  const itemId = current.items.data[0]?.id;
1692
+ const currentStripePriceId = String(current.items.data[0]?.price?.id || '').trim();
1693
+ const currentSubscriptionInterval = parseInterval(current.items.data[0]?.price?.recurring?.interval || '');
1694
+ if (currentStripePriceId === priceId && currentSubscriptionInterval === interval) {
1695
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1696
+ res.end(JSON.stringify({
1697
+ checkoutState: 'already_current_plan',
1698
+ planVersionId,
1699
+ message: 'You are already on this plan.'
1700
+ }));
1701
+ return;
1702
+ }
1819
1703
  if (!itemId) {
1820
1704
  res.writeHead(400, { 'Content-Type': 'application/json' });
1821
1705
  res.end(JSON.stringify({ error: 'Subscription has no updatable line item.' }));
@@ -1840,7 +1724,7 @@ export function registerBillingRoutes(deps) {
1840
1724
  res.end(JSON.stringify({
1841
1725
  checkoutState: 'updated',
1842
1726
  planVersionId,
1843
- message: 'Your subscription was updated successfully.'
1727
+ message: 'Your subscription update was submitted. Changes will appear after Stripe confirms the update.'
1844
1728
  }));
1845
1729
  return;
1846
1730
  }
@@ -2010,20 +1894,52 @@ export function registerBillingRoutes(deps) {
2010
1894
  return;
2011
1895
  }
2012
1896
  }
1897
+ if (!subscriptionId || !subscription) {
1898
+ res.writeHead(409, { 'Content-Type': 'application/json' });
1899
+ res.end(JSON.stringify({
1900
+ error: 'Checkout session is still finalizing your subscription.',
1901
+ code: 'CHECKOUT_SESSION_INCOMPLETE'
1902
+ }));
1903
+ return;
1904
+ }
2013
1905
  core.createUserBillingRecord(resolvedUserId);
2014
1906
  upsertBillingMirror(core, {
2015
1907
  userId: resolvedUserId,
2016
1908
  stripeCustomerId: customerId || null,
2017
1909
  stripeSubscriptionId: subscriptionId || null
2018
1910
  });
2019
- if (subscription) {
2020
- resolvedPlanVersionId = applySubscriptionSnapshot(core, {
2021
- userId: resolvedUserId,
2022
- subscription,
2023
- source: 'checkout-confirm'
2024
- }).planVersionId;
2025
- updateCheckoutAttemptStatus(core, sessionId, 'confirmed');
1911
+ const snapshot = applySubscriptionSnapshot(core, {
1912
+ userId: resolvedUserId,
1913
+ subscription,
1914
+ source: 'checkout-confirm'
1915
+ });
1916
+ resolvedPlanVersionId = snapshot.planVersionId;
1917
+ if (!resolvedPlanVersionId) {
1918
+ res.writeHead(409, { 'Content-Type': 'application/json' });
1919
+ res.end(JSON.stringify({
1920
+ error: 'Checkout completed, but the purchased plan could not be resolved yet.',
1921
+ code: 'CHECKOUT_PLAN_VERSION_UNRESOLVED'
1922
+ }));
1923
+ return;
1924
+ }
1925
+ const updatedEntitlement = getEntitlementRow(core, resolvedUserId);
1926
+ const updatedPlanVersionId = String(updatedEntitlement?.plan_version_id || '').trim();
1927
+ const updatedState = String(updatedEntitlement?.entitlement_state || '').trim().toLowerCase();
1928
+ const canonicalSettled = snapshot.canonicalState !== 'checkout_pending'
1929
+ && updatedPlanVersionId === resolvedPlanVersionId
1930
+ && updatedState === snapshot.canonicalState;
1931
+ if (!canonicalSettled) {
1932
+ console.warn(`[Taskforce Billing] Checkout confirmation still settling for user=${resolvedUserId} session=${sessionId} subscription=${subscriptionId || '(missing)'} `
1933
+ + `planVersionId=${resolvedPlanVersionId} canonicalState=${snapshot.canonicalState} persistedPlanVersionId=${updatedPlanVersionId || '(missing)'} `
1934
+ + `persistedState=${updatedState || '(missing)'}`);
1935
+ res.writeHead(409, { 'Content-Type': 'application/json' });
1936
+ res.end(JSON.stringify({
1937
+ error: 'Checkout session is still finalizing your plan.',
1938
+ code: 'CHECKOUT_SESSION_INCOMPLETE'
1939
+ }));
1940
+ return;
2026
1941
  }
1942
+ updateCheckoutAttemptStatus(core, sessionId, 'confirmed');
2027
1943
  res.writeHead(200, { 'Content-Type': 'application/json' });
2028
1944
  res.end(JSON.stringify({
2029
1945
  success: true,
@@ -2088,7 +2004,7 @@ export function registerBillingRoutes(deps) {
2088
2004
  const baseUrl = resolveBaseUrl(req);
2089
2005
  const session = await stripe.billingPortal.sessions.create({
2090
2006
  customer: customerId,
2091
- return_url: `${baseUrl}/settings`
2007
+ return_url: `${baseUrl}/?screen=plans`
2092
2008
  });
2093
2009
  res.writeHead(200, { 'Content-Type': 'application/json' });
2094
2010
  res.end(JSON.stringify({ url: session.url, returnBaseUrl: baseUrl }));