@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
@@ -6,6 +6,8 @@ const BILLING_CONFIG_KEY_PLAN = 'billing.config.v1';
6
6
  const PRICE_METADATA_PLAN_VERSION = 'taskforce_plan_version_id';
7
7
  const BILLING_DIAGNOSTIC_RETENTION_DAYS = 30;
8
8
  const BILLING_DIAGNOSTIC_MAX_PAYLOAD_BYTES = 8 * 1024;
9
+ const BILLING_CHECKOUT_ATTEMPT_RETENTION_DAYS = 14;
10
+ const BILLING_STATUS_RECONCILIATION_TTL_MS = 30_000;
9
11
  const TASKFORCE_PROXY_ORIGIN_HOST_HEADER = 'x-taskforce-origin-host';
10
12
  const TASKFORCE_PROXY_ORIGIN_PROTO_HEADER = 'x-taskforce-origin-proto';
11
13
  const TASKFORCE_PROXY_ORIGIN_BASE_URL_HEADER = 'x-taskforce-origin-base-url';
@@ -25,6 +27,15 @@ function isoFromUnixSeconds(raw) {
25
27
  return null;
26
28
  return new Date(Math.floor(value) * 1000).toISOString();
27
29
  }
30
+ function isIsoTimestampExpired(valueRaw) {
31
+ const value = String(valueRaw || '').trim();
32
+ if (!value)
33
+ return false;
34
+ const timestamp = Date.parse(value);
35
+ if (!Number.isFinite(timestamp))
36
+ return false;
37
+ return timestamp <= Date.now();
38
+ }
28
39
  function parseInterval(raw) {
29
40
  const value = String(raw || '').trim().toLowerCase();
30
41
  if (value === 'month' || value === 'year')
@@ -229,6 +240,8 @@ function mapStripeStatusToCanonicalState(statusRaw) {
229
240
  return 'active';
230
241
  if (status === 'past_due')
231
242
  return 'grace';
243
+ if (status === 'incomplete')
244
+ return 'checkout_pending';
232
245
  return 'canceled';
233
246
  }
234
247
  function isMissingStripeCustomerError(error) {
@@ -473,14 +486,304 @@ function getEntitlementRow(core, userIdRaw) {
473
486
  const db = core.getDatabaseAdapter();
474
487
  const tenantId = core.getTenantId();
475
488
  const row = db.prepare(`
476
- SELECT account_id, plan_version_id, entitlement_state
489
+ SELECT account_id, plan_version_id, entitlement_state, effective_until, updated_by
477
490
  FROM account_entitlements
478
491
  WHERE tenant_id = ? AND account_id = ?
479
492
  LIMIT 1
480
493
  `).get(tenantId, userId);
481
494
  return row || null;
482
495
  }
496
+ function updateCheckoutAttemptStatus(core, sessionIdRaw, status) {
497
+ const sessionId = String(sessionIdRaw || '').trim();
498
+ if (!sessionId)
499
+ return;
500
+ const db = core.getDatabaseAdapter();
501
+ const tenantId = core.getTenantId();
502
+ db.prepare(`
503
+ UPDATE billing_checkout_attempts
504
+ SET status = ?, updated_at = ?
505
+ WHERE tenant_id = ? AND session_id = ?
506
+ `).run(status, new Date().toISOString(), tenantId, sessionId);
507
+ }
508
+ function expireOpenCheckoutAttempts(core) {
509
+ const db = core.getDatabaseAdapter();
510
+ const tenantId = core.getTenantId();
511
+ const now = new Date().toISOString();
512
+ const result = db.prepare(`
513
+ UPDATE billing_checkout_attempts
514
+ SET status = 'expired', updated_at = ?
515
+ WHERE tenant_id = ?
516
+ AND status = 'open'
517
+ AND expires_at IS NOT NULL
518
+ AND expires_at <> ''
519
+ AND expires_at <= ?
520
+ `).run(now, tenantId, now);
521
+ return Number(result?.changes || 0);
522
+ }
523
+ function purgeTerminalCheckoutAttempts(core) {
524
+ const db = core.getDatabaseAdapter();
525
+ const tenantId = core.getTenantId();
526
+ const cutoff = new Date(Date.now() - BILLING_CHECKOUT_ATTEMPT_RETENTION_DAYS * 24 * 60 * 60 * 1000).toISOString();
527
+ const result = db.prepare(`
528
+ DELETE FROM billing_checkout_attempts
529
+ WHERE tenant_id = ?
530
+ AND status IN ('confirmed', 'superseded', 'expired')
531
+ AND updated_at <= ?
532
+ `).run(tenantId, cutoff);
533
+ return Number(result?.changes || 0);
534
+ }
535
+ function maintainCheckoutAttempts(core) {
536
+ const expiredRows = expireOpenCheckoutAttempts(core);
537
+ const purgedRows = purgeTerminalCheckoutAttempts(core);
538
+ return { expiredRows, purgedRows };
539
+ }
540
+ function normalizeCheckoutAttemptRow(core, row) {
541
+ if (!row)
542
+ return null;
543
+ const sessionId = String(row.session_id || '').trim();
544
+ const status = String(row.status || '').trim().toLowerCase();
545
+ if (!sessionId || status !== 'open' || !isIsoTimestampExpired(row.expires_at)) {
546
+ return row;
547
+ }
548
+ updateCheckoutAttemptStatus(core, sessionId, 'expired');
549
+ return {
550
+ ...row,
551
+ status: 'expired',
552
+ updated_at: new Date().toISOString()
553
+ };
554
+ }
555
+ function getCheckoutAttemptRow(core, sessionIdRaw) {
556
+ const sessionId = String(sessionIdRaw || '').trim();
557
+ if (!sessionId)
558
+ return null;
559
+ const db = core.getDatabaseAdapter();
560
+ const tenantId = core.getTenantId();
561
+ const row = db.prepare(`
562
+ SELECT session_id, user_id, plan_version_id, billing_interval, status, expires_at, created_at, updated_at
563
+ FROM billing_checkout_attempts
564
+ WHERE tenant_id = ? AND session_id = ?
565
+ LIMIT 1
566
+ `).get(tenantId, sessionId);
567
+ return normalizeCheckoutAttemptRow(core, row || null);
568
+ }
569
+ function getCurrentOpenCheckoutAttemptForUser(core, userIdRaw) {
570
+ const userId = String(userIdRaw || '').trim();
571
+ if (!userId)
572
+ return null;
573
+ const db = core.getDatabaseAdapter();
574
+ const tenantId = core.getTenantId();
575
+ const rows = db.prepare(`
576
+ SELECT session_id, user_id, plan_version_id, billing_interval, status, expires_at, created_at, updated_at
577
+ FROM billing_checkout_attempts
578
+ WHERE tenant_id = ? AND user_id = ? AND status = 'open'
579
+ ORDER BY created_at DESC, session_id DESC
580
+ `).all(tenantId, userId);
581
+ for (const row of rows) {
582
+ const normalized = normalizeCheckoutAttemptRow(core, row);
583
+ if (String(normalized?.status || '').trim().toLowerCase() === 'open') {
584
+ return normalized;
585
+ }
586
+ }
587
+ return null;
588
+ }
589
+ function getLatestCheckoutAttemptForUser(core, userIdRaw) {
590
+ const userId = String(userIdRaw || '').trim();
591
+ if (!userId)
592
+ return null;
593
+ const db = core.getDatabaseAdapter();
594
+ const tenantId = core.getTenantId();
595
+ const row = db.prepare(`
596
+ SELECT session_id, user_id, plan_version_id, billing_interval, status, expires_at, created_at, updated_at
597
+ FROM billing_checkout_attempts
598
+ WHERE tenant_id = ? AND user_id = ?
599
+ ORDER BY created_at DESC, session_id DESC
600
+ LIMIT 1
601
+ `).get(tenantId, userId);
602
+ return normalizeCheckoutAttemptRow(core, row || null);
603
+ }
604
+ function resolveBillingConflictCode(core, input) {
605
+ const gate = String(input.gate || '').trim().toLowerCase();
606
+ if (!gate || gate === 'ok')
607
+ return null;
608
+ const latestAttempt = getLatestCheckoutAttemptForUser(core, input.userId);
609
+ const attemptStatus = String(latestAttempt?.status || '').trim().toLowerCase();
610
+ if (attemptStatus === 'expired')
611
+ return 'CHECKOUT_SESSION_EXPIRED';
612
+ if (attemptStatus === 'superseded')
613
+ return 'CHECKOUT_SESSION_SUPERSEDED';
614
+ return null;
615
+ }
616
+ function markLatestOpenCheckoutAttemptConfirmed(core, input) {
617
+ const planVersionId = String(input.planVersionId || '').trim();
618
+ if (!planVersionId)
619
+ return;
620
+ const currentAttempt = getCurrentOpenCheckoutAttemptForUser(core, input.userId);
621
+ if (!currentAttempt)
622
+ return;
623
+ const currentPlanVersionId = String(currentAttempt.plan_version_id || '').trim();
624
+ const sessionId = String(currentAttempt.session_id || '').trim();
625
+ if (!sessionId || currentPlanVersionId !== planVersionId)
626
+ return;
627
+ updateCheckoutAttemptStatus(core, sessionId, 'confirmed');
628
+ }
629
+ function recordCheckoutAttempt(core, input) {
630
+ const sessionId = String(input.sessionId || '').trim();
631
+ const userId = String(input.userId || '').trim();
632
+ const planVersionId = String(input.planVersionId || '').trim();
633
+ const billingInterval = parseInterval(input.billingInterval);
634
+ if (!sessionId || !userId || !planVersionId || !billingInterval)
635
+ return;
636
+ const db = core.getDatabaseAdapter();
637
+ const tenantId = core.getTenantId();
638
+ const now = new Date().toISOString();
639
+ const tx = db.transaction(() => {
640
+ db.prepare(`
641
+ UPDATE billing_checkout_attempts
642
+ SET status = 'superseded', updated_at = ?
643
+ WHERE tenant_id = ? AND user_id = ? AND status = 'open' AND session_id <> ?
644
+ `).run(now, tenantId, userId, sessionId);
645
+ db.prepare(`
646
+ INSERT INTO billing_checkout_attempts (
647
+ session_id, tenant_id, user_id, plan_version_id, billing_interval, status, expires_at, created_at, updated_at
648
+ ) VALUES (?, ?, ?, ?, ?, 'open', ?, ?, ?)
649
+ ON CONFLICT (session_id) DO UPDATE SET
650
+ user_id = excluded.user_id,
651
+ plan_version_id = excluded.plan_version_id,
652
+ billing_interval = excluded.billing_interval,
653
+ status = 'open',
654
+ expires_at = excluded.expires_at,
655
+ updated_at = excluded.updated_at
656
+ `).run(sessionId, tenantId, userId, planVersionId, billingInterval, input.expiresAt || null, now, now);
657
+ });
658
+ tx();
659
+ }
660
+ function resolveCheckoutLifecycleGuard(core, input) {
661
+ const currentOpenAttempt = getCurrentOpenCheckoutAttemptForUser(core, input.userId);
662
+ const checkoutAttempt = input.sessionId
663
+ ? getCheckoutAttemptRow(core, input.sessionId)
664
+ : currentOpenAttempt;
665
+ const attemptStatus = String(checkoutAttempt?.status || '').trim().toLowerCase();
666
+ const attemptPlanVersionId = String(checkoutAttempt?.plan_version_id || '').trim();
667
+ if (attemptStatus === 'expired') {
668
+ return {
669
+ blocked: true,
670
+ code: 'CHECKOUT_SESSION_EXPIRED',
671
+ message: 'This checkout session expired. Choose a plan and start checkout again.'
672
+ };
673
+ }
674
+ if (attemptStatus === 'superseded') {
675
+ return {
676
+ blocked: true,
677
+ code: 'CHECKOUT_SESSION_SUPERSEDED',
678
+ message: 'This checkout session is no longer current for this account.'
679
+ };
680
+ }
681
+ if (attemptStatus === 'open' && attemptPlanVersionId && input.planVersionId && attemptPlanVersionId !== input.planVersionId) {
682
+ return {
683
+ blocked: true,
684
+ code: 'CHECKOUT_SESSION_SUPERSEDED',
685
+ message: 'This checkout session is no longer current for this account.'
686
+ };
687
+ }
688
+ const currentBilling = getBillingMirrorRow(core, input.userId);
689
+ const currentSubscriptionId = String(currentBilling?.stripe_subscription_id || '').trim();
690
+ if (!currentOpenAttempt && input.subscriptionId && currentSubscriptionId && currentSubscriptionId !== input.subscriptionId) {
691
+ return {
692
+ blocked: true,
693
+ code: 'CHECKOUT_SESSION_SUPERSEDED',
694
+ message: 'This checkout session is no longer current for this account.'
695
+ };
696
+ }
697
+ const entitlement = getEntitlementRow(core, input.userId);
698
+ if (!entitlement)
699
+ return { blocked: false };
700
+ const currentState = String(entitlement.entitlement_state || '').trim().toLowerCase();
701
+ const currentPlanVersionId = String(entitlement.plan_version_id || '').trim();
702
+ const updatedBy = String(entitlement.updated_by || '').trim().toLowerCase();
703
+ const expiredPendingCheckout = currentState === 'checkout_pending' && isIsoTimestampExpired(entitlement.effective_until);
704
+ if (expiredPendingCheckout || (currentState === 'pending_plan_selection' && updatedBy === 'checkout-expired')) {
705
+ return {
706
+ blocked: true,
707
+ code: 'CHECKOUT_SESSION_EXPIRED',
708
+ message: 'This checkout session expired. Choose a plan and start checkout again.'
709
+ };
710
+ }
711
+ if (currentState === 'checkout_pending'
712
+ && currentPlanVersionId
713
+ && input.planVersionId
714
+ && currentPlanVersionId !== input.planVersionId) {
715
+ return {
716
+ blocked: true,
717
+ code: 'CHECKOUT_SESSION_SUPERSEDED',
718
+ message: 'This checkout session is no longer current for this account.'
719
+ };
720
+ }
721
+ return { blocked: false };
722
+ }
723
+ function isCommercialStateAppAccessible(stateRaw) {
724
+ const state = String(stateRaw || '').trim().toLowerCase();
725
+ return state === 'active' || state === 'trialing' || state === 'grace';
726
+ }
727
+ function shouldRecordCheckoutPendingState(entitlement) {
728
+ if (!entitlement)
729
+ return true;
730
+ const currentState = String(entitlement.entitlement_state || '').trim().toLowerCase();
731
+ return currentState === 'pending_plan_selection'
732
+ || currentState === 'checkout_pending'
733
+ || currentState === 'canceled';
734
+ }
735
+ function beginCheckoutPending(core, input) {
736
+ const planId = resolvePlanIdForVersion(core, input.planVersionId);
737
+ const updatedBy = input.updatedBy || 'checkout-start';
738
+ const source = input.source || 'checkout-start';
739
+ if (typeof core.transitionCanonicalCommercialState === 'function') {
740
+ core.transitionCanonicalCommercialState({
741
+ accountId: input.userId,
742
+ planVersionId: input.planVersionId,
743
+ state: 'checkout_pending',
744
+ effectiveUntil: input.effectiveUntil || null,
745
+ billingMirrorPlanId: planId,
746
+ source,
747
+ updatedBy
748
+ });
749
+ return;
750
+ }
751
+ const db = core.getDatabaseAdapter();
752
+ const tenantId = core.getTenantId();
753
+ const now = new Date().toISOString();
754
+ db.prepare(`
755
+ INSERT INTO account_entitlements (account_id, tenant_id, plan_version_id, entitlement_state, effective_at, effective_until, updated_by, created_at, updated_at)
756
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
757
+ ON CONFLICT (account_id) DO UPDATE SET
758
+ plan_version_id = excluded.plan_version_id,
759
+ entitlement_state = excluded.entitlement_state,
760
+ effective_at = excluded.effective_at,
761
+ effective_until = excluded.effective_until,
762
+ updated_by = excluded.updated_by,
763
+ updated_at = excluded.updated_at
764
+ `).run(input.userId, tenantId, input.planVersionId, 'checkout_pending', now, input.effectiveUntil || null, updatedBy, now, now);
765
+ upsertBillingMirror(core, {
766
+ userId: input.userId,
767
+ planId,
768
+ canonicalState: 'checkout_pending',
769
+ effectiveUntil: input.effectiveUntil || null
770
+ });
771
+ }
483
772
  function upsertCanonicalEntitlement(core, input) {
773
+ const updatedBy = input.updatedBy || 'stripe-webhook';
774
+ if (typeof core.transitionCanonicalCommercialState === 'function') {
775
+ core.transitionCanonicalCommercialState({
776
+ accountId: input.userId,
777
+ planVersionId: input.planVersionId,
778
+ state: input.state,
779
+ effectiveAt: input.effectiveAt,
780
+ effectiveUntil: input.effectiveUntil,
781
+ billingMirrorPlanId: resolvePlanIdForVersion(core, input.planVersionId),
782
+ source: updatedBy === 'checkout-confirm' ? 'checkout-confirm' : 'stripe-webhook',
783
+ updatedBy
784
+ });
785
+ return;
786
+ }
484
787
  const db = core.getDatabaseAdapter();
485
788
  const tenantId = core.getTenantId();
486
789
  const now = new Date().toISOString();
@@ -494,7 +797,7 @@ function upsertCanonicalEntitlement(core, input) {
494
797
  effective_until = excluded.effective_until,
495
798
  updated_by = excluded.updated_by,
496
799
  updated_at = excluded.updated_at
497
- `).run(input.userId, tenantId, input.planVersionId, input.state, input.effectiveAt || now, input.effectiveUntil || null, input.updatedBy || 'stripe-webhook', now, now);
800
+ `).run(input.userId, tenantId, input.planVersionId, input.state, input.effectiveAt || now, input.effectiveUntil || null, updatedBy, now, now);
498
801
  }
499
802
  function upsertBillingMirror(core, input) {
500
803
  const db = core.getDatabaseAdapter();
@@ -545,7 +848,12 @@ function applySubscriptionSnapshot(core, input) {
545
848
  const canceledState = stripeStatus === 'canceled'
546
849
  ? (currentPeriodEnd && currentPeriodEnd > nowIso ? 'active' : 'canceled')
547
850
  : null;
548
- const canonicalState = canceledState || mapStripeStatusToCanonicalState(stripeStatus);
851
+ const mappedCanonicalState = canceledState || mapStripeStatusToCanonicalState(stripeStatus);
852
+ const preserveExistingAccess = mappedCanonicalState === 'checkout_pending'
853
+ && isCommercialStateAppAccessible(existingEntitlement?.entitlement_state);
854
+ const canonicalState = preserveExistingAccess
855
+ ? String(existingEntitlement?.entitlement_state || 'active').trim().toLowerCase()
856
+ : mappedCanonicalState;
549
857
  const effectiveUntil = stripeStatus === 'canceled'
550
858
  ? currentPeriodEnd
551
859
  : (currentPeriodEnd || null);
@@ -562,7 +870,17 @@ function applySubscriptionSnapshot(core, input) {
562
870
  currentPeriodEnd,
563
871
  effectiveUntil
564
872
  });
565
- if (planVersionId) {
873
+ if (planVersionId && canonicalState === 'checkout_pending') {
874
+ if (shouldRecordCheckoutPendingState(existingEntitlement)) {
875
+ beginCheckoutPending(core, {
876
+ userId,
877
+ planVersionId,
878
+ source,
879
+ updatedBy: source
880
+ });
881
+ }
882
+ }
883
+ else if (planVersionId) {
566
884
  upsertCanonicalEntitlement(core, {
567
885
  userId,
568
886
  planVersionId: planVersionId || existingEntitlement?.plan_version_id || core.getDefaultPlanVersionIdByMode('personal', { allowFallback: true }),
@@ -623,12 +941,18 @@ function getBillingMirrorRow(core, userIdRaw) {
623
941
  const db = core.getDatabaseAdapter();
624
942
  const tenantId = core.getTenantId();
625
943
  return db.prepare(`
626
- SELECT stripe_status, stripe_customer_id, stripe_subscription_id, stripe_price_id, billing_interval, trial_end, current_period_end, effective_until, entitlement_state, tier
944
+ SELECT stripe_status, stripe_customer_id, stripe_subscription_id, stripe_price_id, billing_interval, trial_end, current_period_end, effective_until, entitlement_state, tier, updated_at
627
945
  FROM user_billing
628
946
  WHERE tenant_id = ? AND user_id = ?
629
947
  LIMIT 1
630
948
  `).get(tenantId, userId);
631
949
  }
950
+ function isBillingMirrorFresh(row) {
951
+ const updatedAt = Date.parse(String(row?.updated_at || ''));
952
+ if (!Number.isFinite(updatedAt))
953
+ return false;
954
+ return (Date.now() - updatedAt) < BILLING_STATUS_RECONCILIATION_TTL_MS;
955
+ }
632
956
  function isStripeManagedSubscriptionStatus(statusRaw) {
633
957
  const status = String(statusRaw || '').trim().toLowerCase();
634
958
  return status === 'trialing'
@@ -708,6 +1032,9 @@ async function reconcileBillingMirrorIfNeeded(core, userIdRaw) {
708
1032
  if (!userId)
709
1033
  return { row: null, conflictCode: null };
710
1034
  let row = getBillingMirrorRow(core, userId);
1035
+ if (isBillingMirrorFresh(row)) {
1036
+ return { row, conflictCode: null };
1037
+ }
711
1038
  if (String(row?.stripe_subscription_id || '').trim()) {
712
1039
  try {
713
1040
  await reconcileBillingFromStripe(core, userId, row?.stripe_subscription_id);
@@ -924,12 +1251,23 @@ export function registerBillingRoutes(deps) {
924
1251
  return;
925
1252
  }
926
1253
  try {
1254
+ maintainCheckoutAttempts(core);
927
1255
  const userId = requestUserId(req);
928
1256
  const db = core.getDatabaseAdapter();
929
1257
  const tenantId = core.getTenantId();
930
- const billingReconciliation = await reconcileBillingMirrorIfNeeded(core, userId);
931
- const row = billingReconciliation.row;
1258
+ const row = getBillingMirrorRow(core, userId);
932
1259
  const access = core.getAccountAccessStatus(userId);
1260
+ const billingConflictCode = resolveBillingConflictCode(core, {
1261
+ userId,
1262
+ gate: String(access.gate || '')
1263
+ });
1264
+ const resolvedPlanId = String(access.entitlement?.planId || row?.tier || '').trim() || null;
1265
+ const planRow = resolvedPlanId ? db.prepare(`
1266
+ SELECT display_name
1267
+ FROM plan_catalog
1268
+ WHERE tenant_id = ? AND plan_id = ?
1269
+ LIMIT 1
1270
+ `).get(tenantId, resolvedPlanId) : undefined;
933
1271
  if (!access.hasEntitlement || !access.entitlement) {
934
1272
  res.writeHead(200, { 'Content-Type': 'application/json' });
935
1273
  res.end(JSON.stringify({
@@ -941,24 +1279,18 @@ export function registerBillingRoutes(deps) {
941
1279
  trialEnd: row?.trial_end || null,
942
1280
  currentPeriodEnd: row?.current_period_end || null,
943
1281
  effectiveUntil: row?.effective_until || null,
944
- entitlementState: null,
1282
+ entitlementState: row?.entitlement_state || null,
945
1283
  planVersionId: null,
946
- planId: null,
947
- planName: null,
1284
+ planId: resolvedPlanId,
1285
+ planName: planRow?.display_name ? String(planRow.display_name) : resolvedPlanId,
948
1286
  gate: access.gate,
949
1287
  message: access.message,
950
1288
  planSelectionRequired: access.planSelectionRequired,
951
- billingConflictCode: billingReconciliation.conflictCode
1289
+ billingConflictCode
952
1290
  }));
953
1291
  return;
954
1292
  }
955
1293
  const entitlement = access.entitlement;
956
- const planRow = db.prepare(`
957
- SELECT display_name
958
- FROM plan_catalog
959
- WHERE tenant_id = ? AND plan_id = ?
960
- LIMIT 1
961
- `).get(tenantId, entitlement.planId);
962
1294
  res.writeHead(200, { 'Content-Type': 'application/json' });
963
1295
  res.end(JSON.stringify({
964
1296
  stripeStatus: row?.stripe_status || null,
@@ -976,7 +1308,7 @@ export function registerBillingRoutes(deps) {
976
1308
  gate: access.gate,
977
1309
  message: access.message,
978
1310
  planSelectionRequired: access.planSelectionRequired,
979
- billingConflictCode: billingReconciliation.conflictCode
1311
+ billingConflictCode
980
1312
  }));
981
1313
  }
982
1314
  catch (error) {
@@ -1280,6 +1612,68 @@ export function registerBillingRoutes(deps) {
1280
1612
  rows
1281
1613
  }));
1282
1614
  });
1615
+ addRoute('GET', '/api/taskforce/admin/billing/checkout-attempts', async (req, res) => {
1616
+ if (!ensureCloudBilling(req, res))
1617
+ return;
1618
+ if (!ensureAdminRole(req)) {
1619
+ res.writeHead(403, { 'Content-Type': 'application/json' });
1620
+ res.end(JSON.stringify({ error: 'Forbidden: system admin role required.' }));
1621
+ return;
1622
+ }
1623
+ const db = core.getDatabaseAdapter();
1624
+ const tenantId = core.getTenantId();
1625
+ const requestUrl = parseRequestUrl(req);
1626
+ const limit = parsePositiveIntBounded(requestUrl.searchParams.get('limit'), 50, 1, 200);
1627
+ const filterUserId = String(requestUrl.searchParams.get('userId') || '').trim();
1628
+ const now = new Date().toISOString();
1629
+ const maintenance = maintainCheckoutAttempts(core);
1630
+ const rows = filterUserId
1631
+ ? db.prepare(`
1632
+ SELECT session_id, user_id, plan_version_id, billing_interval, status, expires_at, created_at, updated_at
1633
+ FROM billing_checkout_attempts
1634
+ WHERE tenant_id = ? AND user_id = ?
1635
+ ORDER BY created_at DESC, session_id DESC
1636
+ LIMIT ?
1637
+ `).all(tenantId, filterUserId, limit)
1638
+ : db.prepare(`
1639
+ SELECT session_id, user_id, plan_version_id, billing_interval, status, expires_at, created_at, updated_at
1640
+ FROM billing_checkout_attempts
1641
+ WHERE tenant_id = ?
1642
+ ORDER BY created_at DESC, session_id DESC
1643
+ LIMIT ?
1644
+ `).all(tenantId, limit);
1645
+ const total = filterUserId
1646
+ ? db.prepare(`
1647
+ SELECT COUNT(*) AS count
1648
+ FROM billing_checkout_attempts
1649
+ WHERE tenant_id = ? AND user_id = ?
1650
+ `).get(tenantId, filterUserId)
1651
+ : db.prepare(`
1652
+ SELECT COUNT(*) AS count
1653
+ FROM billing_checkout_attempts
1654
+ WHERE tenant_id = ?
1655
+ `).get(tenantId);
1656
+ const openCount = filterUserId
1657
+ ? db.prepare(`
1658
+ SELECT COUNT(*) AS count
1659
+ FROM billing_checkout_attempts
1660
+ WHERE tenant_id = ? AND user_id = ? AND status = 'open'
1661
+ `).get(tenantId, filterUserId)
1662
+ : db.prepare(`
1663
+ SELECT COUNT(*) AS count
1664
+ FROM billing_checkout_attempts
1665
+ WHERE tenant_id = ? AND status = 'open'
1666
+ `).get(tenantId);
1667
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1668
+ res.end(JSON.stringify({
1669
+ asOf: now,
1670
+ expiredRowsNormalized: maintenance.expiredRows,
1671
+ purgedTerminalRows: maintenance.purgedRows,
1672
+ total: Number(total?.count || 0),
1673
+ openCount: Number(openCount?.count || 0),
1674
+ rows
1675
+ }));
1676
+ });
1283
1677
  addRoute('POST', '/api/taskforce/admin/billing/prices/import', async (req, res) => {
1284
1678
  if (!ensureCloudBilling(req, res))
1285
1679
  return;
@@ -1378,6 +1772,7 @@ export function registerBillingRoutes(deps) {
1378
1772
  return;
1379
1773
  }
1380
1774
  try {
1775
+ maintainCheckoutAttempts(core);
1381
1776
  const userId = requestUserId(req);
1382
1777
  const currentBillingResult = await reconcileBillingMirrorIfNeeded(core, userId);
1383
1778
  const currentBilling = currentBillingResult.row;
@@ -1541,6 +1936,21 @@ export function registerBillingRoutes(deps) {
1541
1936
  billingInterval: interval
1542
1937
  }
1543
1938
  });
1939
+ const checkoutEffectiveUntil = isoFromUnixSeconds(session.expires_at);
1940
+ recordCheckoutAttempt(core, {
1941
+ sessionId: session.id,
1942
+ userId,
1943
+ planVersionId,
1944
+ billingInterval: interval,
1945
+ expiresAt: checkoutEffectiveUntil
1946
+ });
1947
+ if (shouldRecordCheckoutPendingState(currentEntitlement) && !isCommercialStateAppAccessible(currentEntitlement?.entitlement_state)) {
1948
+ beginCheckoutPending(core, {
1949
+ userId,
1950
+ planVersionId,
1951
+ effectiveUntil: checkoutEffectiveUntil
1952
+ });
1953
+ }
1544
1954
  const checkoutUrl = isMockStripeClient(stripe)
1545
1955
  ? buildMockStripeHostedCheckoutUrl(resolveHostedServerBaseUrl(req), session.id)
1546
1956
  : (session.url || null);
@@ -1582,11 +1992,30 @@ export function registerBillingRoutes(deps) {
1582
1992
  return;
1583
1993
  }
1584
1994
  try {
1995
+ maintainCheckoutAttempts(core);
1585
1996
  const stripe = getStripeClient();
1586
1997
  const expectedUserId = requestUserId(req);
1587
1998
  const session = await stripe.checkout.sessions.retrieve(sessionId, {
1588
1999
  expand: ['subscription']
1589
2000
  });
2001
+ const sessionStatus = String(session.status || '').trim().toLowerCase();
2002
+ const paymentStatus = String(session.payment_status || '').trim().toLowerCase();
2003
+ if (session.mode !== 'subscription') {
2004
+ res.writeHead(409, { 'Content-Type': 'application/json' });
2005
+ res.end(JSON.stringify({
2006
+ error: 'Checkout session is not a subscription checkout.',
2007
+ code: 'CHECKOUT_SESSION_INVALID_MODE'
2008
+ }));
2009
+ return;
2010
+ }
2011
+ if (sessionStatus !== 'complete' || (paymentStatus !== 'paid' && paymentStatus !== 'no_payment_required')) {
2012
+ res.writeHead(409, { 'Content-Type': 'application/json' });
2013
+ res.end(JSON.stringify({
2014
+ error: 'Checkout session has not completed yet.',
2015
+ code: 'CHECKOUT_SESSION_INCOMPLETE'
2016
+ }));
2017
+ return;
2018
+ }
1590
2019
  const metadata = session.metadata || {};
1591
2020
  const customerId = String(session.customer || '').trim();
1592
2021
  const subscriptionId = typeof session.subscription === 'string'
@@ -1605,22 +2034,47 @@ export function registerBillingRoutes(deps) {
1605
2034
  res.end(JSON.stringify({ error: 'Checkout session does not belong to the current account.' }));
1606
2035
  return;
1607
2036
  }
2037
+ let resolvedPlanVersionId = null;
2038
+ let subscription = null;
2039
+ if (subscriptionId) {
2040
+ subscription = typeof session.subscription === 'string' || !session.subscription
2041
+ ? await stripe.subscriptions.retrieve(subscriptionId)
2042
+ : session.subscription;
2043
+ const priceId = String(subscription.items?.data?.[0]?.price?.id || '').trim();
2044
+ resolvedPlanVersionId = resolvePlanVersionIdForBillingEvent(core, [
2045
+ subscription.metadata?.planVersionId,
2046
+ subscription.metadata?.plan_version_id,
2047
+ metadata.planVersionId,
2048
+ metadata.plan_version_id
2049
+ ], priceId).planVersionId;
2050
+ const lifecycleGuard = resolveCheckoutLifecycleGuard(core, {
2051
+ userId: resolvedUserId,
2052
+ planVersionId: resolvedPlanVersionId,
2053
+ sessionId,
2054
+ subscriptionId
2055
+ });
2056
+ if (lifecycleGuard.blocked) {
2057
+ res.writeHead(409, { 'Content-Type': 'application/json' });
2058
+ res.end(JSON.stringify({
2059
+ error: lifecycleGuard.message,
2060
+ code: lifecycleGuard.code
2061
+ }));
2062
+ return;
2063
+ }
2064
+ }
1608
2065
  core.createUserBillingRecord(resolvedUserId);
1609
2066
  upsertBillingMirror(core, {
1610
2067
  userId: resolvedUserId,
1611
2068
  stripeCustomerId: customerId || null,
1612
2069
  stripeSubscriptionId: subscriptionId || null
1613
2070
  });
1614
- let resolvedPlanVersionId = null;
1615
- if (session.mode === 'subscription' && subscriptionId) {
1616
- const subscription = typeof session.subscription === 'string' || !session.subscription
1617
- ? await stripe.subscriptions.retrieve(subscriptionId)
1618
- : session.subscription;
2071
+ if (subscription) {
1619
2072
  resolvedPlanVersionId = applySubscriptionSnapshot(core, {
1620
2073
  userId: resolvedUserId,
1621
2074
  subscription,
1622
2075
  source: 'checkout-confirm'
1623
2076
  }).planVersionId;
2077
+ updateCheckoutAttemptStatus(core, sessionId, 'confirmed');
1624
2078
  }
1625
2079
  res.writeHead(200, { 'Content-Type': 'application/json' });
1626
2080
  res.end(JSON.stringify({
@@ -1791,6 +2245,7 @@ export function registerBillingRoutes(deps) {
1791
2245
  const db = core.getDatabaseAdapter();
1792
2246
  const tenantId = core.getTenantId();
1793
2247
  purgeExpiredWebhookFailures(core);
2248
+ maintainCheckoutAttempts(core);
1794
2249
  let duplicate = false;
1795
2250
  const tx = db.transaction(() => {
1796
2251
  const acquired = lockWebhookEvent(core, event);
@@ -1843,11 +2298,31 @@ export function registerBillingRoutes(deps) {
1843
2298
  });
1844
2299
  return;
1845
2300
  }
1846
- applySubscriptionSnapshot(core, {
2301
+ const lifecycleGuard = resolveCheckoutLifecycleGuard(core, {
2302
+ userId,
2303
+ planVersionId,
2304
+ subscriptionId: String(subscription.id || '').trim()
2305
+ });
2306
+ if (lifecycleGuard.blocked) {
2307
+ recordWebhookFailure(core, {
2308
+ eventId: event.id,
2309
+ eventType: event.type,
2310
+ reason: lifecycleGuard.message,
2311
+ payload: event
2312
+ });
2313
+ return;
2314
+ }
2315
+ const snapshot = applySubscriptionSnapshot(core, {
1847
2316
  userId,
1848
2317
  subscription,
1849
2318
  source: 'stripe-webhook'
1850
2319
  });
2320
+ if (snapshot.canonicalState !== 'checkout_pending') {
2321
+ markLatestOpenCheckoutAttemptConfirmed(core, {
2322
+ userId,
2323
+ planVersionId: snapshot.planVersionId
2324
+ });
2325
+ }
1851
2326
  return;
1852
2327
  }
1853
2328
  if (event.type === 'invoice.paid' || event.type === 'invoice.payment_failed') {
@@ -1868,6 +2343,20 @@ export function registerBillingRoutes(deps) {
1868
2343
  invoice.subscription_details?.metadata?.plan_version_id
1869
2344
  ], priceId);
1870
2345
  const planVersionId = planVersionResolution.planVersionId;
2346
+ const lifecycleGuard = resolveCheckoutLifecycleGuard(core, {
2347
+ userId,
2348
+ planVersionId,
2349
+ subscriptionId
2350
+ });
2351
+ if (lifecycleGuard.blocked) {
2352
+ recordWebhookFailure(core, {
2353
+ eventId: event.id,
2354
+ eventType: event.type,
2355
+ reason: lifecycleGuard.message,
2356
+ payload: event
2357
+ });
2358
+ return;
2359
+ }
1871
2360
  const existingEntitlement = getEntitlementRow(core, userId);
1872
2361
  const canonicalState = event.type === 'invoice.payment_failed' ? 'grace' : 'active';
1873
2362
  const currentPeriodEnd = invoice.lines?.data?.[0]?.period?.end
@@ -1903,6 +2392,10 @@ export function registerBillingRoutes(deps) {
1903
2392
  effectiveAt: new Date().toISOString(),
1904
2393
  effectiveUntil: currentPeriodEnd
1905
2394
  });
2395
+ markLatestOpenCheckoutAttemptConfirmed(core, {
2396
+ userId,
2397
+ planVersionId
2398
+ });
1906
2399
  return;
1907
2400
  }
1908
2401
  });