@taskforcehq/taskforce 0.3.302 → 0.3.304
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.
- package/dist/Taskforce.module.css +113 -5
- package/dist/TaskforceCore.js +560 -208
- package/dist/TaskforceCore.test.js +1833 -91
- package/dist/annotatedAttachments/service.d.ts +1 -1
- package/dist/annotatedAttachments/types.d.ts +1 -1
- package/dist/components/features/AgentsModule.js +1 -1
- package/dist/components/features/AgentsModule.test.js +1 -1
- package/dist/components/features/TaskSettings.js +58 -38
- package/dist/components/features/TaskSettings.test.js +139 -40
- package/dist/components/task/TaskActionHeader.d.ts +2 -1
- package/dist/components/task/TaskActionHeader.js +3 -3
- package/dist/components/task/TaskActionHeader.test.js +8 -0
- package/dist/components/task/TaskCard.js +16 -1
- package/dist/components/task/TaskCard.test.js +31 -0
- package/dist/components/task/TaskForm.d.ts +2 -1
- package/dist/components/task/TaskForm.js +22 -4
- package/dist/components/task/TaskForm.test.js +46 -0
- package/dist/components/task/TaskKanban.d.ts +1 -1
- package/dist/components/task/TaskKanban.js +31 -160
- package/dist/components/task/TaskKanban.test.js +17 -0
- package/dist/components/task/TaskStatusActions.d.ts +6 -1
- package/dist/components/task/TaskStatusActions.js +8 -3
- package/dist/components/task/TaskStatusActions.test.js +9 -0
- package/dist/components/task/taskKanbanDropRules.d.ts +58 -0
- package/dist/components/task/taskKanbanDropRules.js +187 -0
- package/dist/components/task/taskKanbanDropRules.test.d.ts +1 -0
- package/dist/components/task/taskKanbanDropRules.test.js +114 -0
- package/dist/components/ui/Modal.d.ts +1 -1
- package/dist/components/ui/Modal.js +1 -0
- package/dist/components/views/AppShellHeader.js +5 -1
- package/dist/components/views/AppShellHeader.test.js +4 -0
- package/dist/components/views/PlanComparisonPage.d.ts +3 -2
- package/dist/components/views/PlanComparisonPage.js +4 -2
- package/dist/components/views/PlanComparisonPage.test.js +4 -0
- package/dist/components/views/PlansPage.d.ts +7 -1
- package/dist/components/views/PlansPage.js +139 -53
- package/dist/components/views/PlansPage.test.d.ts +1 -0
- package/dist/components/views/PlansPage.test.js +157 -0
- package/dist/components/views/StandaloneLayout.d.ts +1 -0
- package/dist/components/views/StandaloneLayout.js +216 -186
- package/dist/components/views/WidgetView.js +11 -2
- package/dist/components/views/standalone/modals/AccountHubModal.d.ts +8 -3
- package/dist/components/views/standalone/modals/AccountHubModal.js +16 -2
- package/dist/components/views/standalone/modals/AccountHubModal.test.d.ts +1 -0
- package/dist/components/views/standalone/modals/AccountHubModal.test.js +26 -0
- package/dist/components/views/standalone/modals/SyncStatusModal.js +1 -1
- package/dist/core/DeletedTaskLifecycleService.d.ts +2 -1
- package/dist/core/EntitlementsPolicy.test.js +2 -2
- package/dist/core/GlobalSettingsService.js +78 -18
- package/dist/core/PlanEntitlementService.d.ts +31 -5
- package/dist/core/PlanEntitlementService.js +350 -222
- package/dist/core/SignupMonetizationSettings.test.js +208 -49
- package/dist/core/SystemAdmin.test.js +186 -96
- package/dist/core/TaskActivityService.d.ts +4 -1
- package/dist/core/TaskActivityService.js +45 -0
- package/dist/core/TaskAuditTimeline.test.js +52 -1
- package/dist/core/Taskforce.d.ts +65 -20
- package/dist/core/Taskforce.js +261 -51
- package/dist/core/Taskforce.mcpAuth.test.js +0 -2
- package/dist/core/UserProfileAvatarDrafts.test.js +12 -3
- package/dist/core/WorkspacePlanMode.test.js +65 -0
- package/dist/core/types.d.ts +11 -12
- package/dist/documentReviews/service.d.ts +1 -1
- package/dist/documentReviews/types.d.ts +1 -1
- package/dist/hooks/auth/useTaskforceAuthBootstrap.d.ts +48 -0
- package/dist/hooks/auth/useTaskforceAuthBootstrap.js +219 -0
- package/dist/hooks/sync/auth.d.ts +37 -0
- package/dist/hooks/sync/auth.js +61 -0
- package/dist/hooks/sync/bootstrap.d.ts +34 -0
- package/dist/hooks/sync/bootstrap.js +52 -0
- package/dist/hooks/sync/orchestratorShared.d.ts +36 -0
- package/dist/hooks/sync/orchestratorShared.js +209 -0
- package/dist/hooks/sync/orchestratorShared.test.d.ts +1 -0
- package/dist/hooks/sync/orchestratorShared.test.js +49 -0
- package/dist/hooks/sync/realtime.d.ts +25 -0
- package/dist/hooks/sync/realtime.js +60 -0
- package/dist/hooks/sync/recovery.d.ts +69 -0
- package/dist/hooks/sync/recovery.js +118 -0
- package/dist/hooks/sync/transfers.d.ts +122 -0
- package/dist/hooks/sync/transfers.js +431 -0
- package/dist/hooks/sync/useDataVersionRefresh.d.ts +16 -0
- package/dist/hooks/sync/useDataVersionRefresh.js +73 -0
- package/dist/hooks/ui/useResourceExport.d.ts +3 -1
- package/dist/hooks/ui/useResourceExport.js +5 -2
- package/dist/hooks/ui/useWorkflowTemplates.js +11 -6
- package/dist/hooks/useRealtimeSync.js +2 -1
- package/dist/hooks/useSyncOrchestrator.aiProfiles.test.js +8 -0
- package/dist/hooks/useSyncOrchestrator.context-assets.test.d.ts +1 -0
- package/dist/hooks/useSyncOrchestrator.context-assets.test.js +228 -0
- package/dist/hooks/useSyncOrchestrator.d.ts +5 -4
- package/dist/hooks/useSyncOrchestrator.js +315 -860
- package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +665 -8
- package/dist/hooks/useTaskMutations.js +45 -6
- package/dist/hooks/useTaskMutations.test.js +147 -0
- package/dist/hooks/useTaskforce.d.ts +11 -2
- package/dist/hooks/useTaskforce.js +290 -812
- package/dist/hooks/useWorkspaceSyncController.d.ts +1 -0
- package/dist/hooks/useWorkspaceSyncController.js +22 -10
- package/dist/hooks/workspace/useTaskforceApiRouting.test.js +17 -0
- package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.d.ts +166 -0
- package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.js +434 -0
- package/dist/hooks/workspace/workspaceLocalState.d.ts +19 -0
- package/dist/hooks/workspace/workspaceLocalState.js +100 -0
- package/dist/hooks/workspace/workspaceLocalState.test.d.ts +1 -0
- package/dist/hooks/workspace/workspaceLocalState.test.js +59 -0
- package/dist/localization/locales/en-US.js +2 -2
- package/dist/mcp/canonicalAssetHelpers.d.ts +70 -0
- package/dist/mcp/canonicalAssetHelpers.js +259 -0
- package/dist/mcp/clientIntegrations.d.ts +12 -1
- package/dist/mcp/clientIntegrations.js +161 -6
- package/dist/mcp/clientIntegrations.test.js +76 -4
- package/dist/mcp/documentHelpers.d.ts +100 -0
- package/dist/mcp/documentHelpers.js +161 -0
- package/dist/mcp/runtime.js +264 -505
- package/dist/mcp/runtime.test.js +353 -89
- package/dist/mcp/taskAttachmentHelpers.d.ts +41 -0
- package/dist/mcp/taskAttachmentHelpers.js +60 -0
- package/dist/mcp/taskAttachmentTypes.d.ts +37 -0
- package/dist/mcp/taskAttachmentTypes.js +1 -0
- package/dist/migrations/billingSchemaParity.test.js +3 -0
- package/dist/migrations/taskSchemaMigrations.js +15 -0
- package/dist/plugins/vite.js +1 -1
- package/dist/runtime/appGateDefinitions.d.ts +1 -1
- package/dist/runtime/appGateDefinitions.js +1 -1
- package/dist/server/index.cookieProxy.test.js +1 -0
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +48 -3
- package/dist/server/index.test.js +14 -2
- package/dist/server/mockStripe.d.ts +1 -0
- package/dist/server/mockStripe.js +2 -0
- package/dist/server/routes/admin.js +91 -40
- package/dist/server/routes/auth.d.ts +1 -0
- package/dist/server/routes/auth.js +231 -55
- package/dist/server/routes/billing.js +473 -39
- package/dist/server/routes/billing.test.js +972 -37
- package/dist/server/routes/shared.d.ts +30 -0
- package/dist/server/routes/shared.js +31 -3
- package/dist/server/routes/sync.integration.test.js +180 -0
- package/dist/server/routes/sync.js +125 -242
- package/dist/server/routes/tasks.js +49 -7
- package/dist/server/routes.js +11 -22
- package/dist/server/routes.test.js +530 -81
- package/dist/shared/taskActor.d.ts +10 -0
- package/dist/shared/taskActor.js +1 -0
- package/dist/sync/cloudSyncApi.d.ts +1 -0
- package/dist/sync/collaborationSyncPayload.js +4 -0
- package/dist/sync/collaborationSyncPayload.test.js +8 -0
- package/dist/sync/collaborationSyncState.d.ts +3 -1
- package/dist/sync/collaborationSyncState.js +19 -0
- package/dist/sync/contentSyncState.d.ts +10 -0
- package/dist/sync/contentSyncState.js +277 -14
- package/dist/sync/syncApplyHandlers.d.ts +87 -1
- package/dist/sync/syncApplyHandlers.js +370 -8
- package/dist/sync/syncApplyHandlers.test.d.ts +1 -0
- package/dist/sync/syncApplyHandlers.test.js +377 -0
- package/dist/sync/syncService.d.ts +9 -1
- package/dist/sync/syncService.js +57 -10
- package/dist/sync/syncService.test.js +48 -0
- package/dist/sync/workspacePullFeed.d.ts +11 -0
- package/dist/sync/workspacePullFeed.js +123 -68
- package/dist/sync/workspacePullFeed.test.js +180 -0
- package/dist/sync/workspaceSyncModel.d.ts +22 -1
- package/dist/sync/workspaceSyncModel.js +65 -0
- package/dist/sync/workspaceSyncModel.test.js +119 -0
- package/dist/types.d.ts +2 -10
- package/dist/ui/assets/AgentsModule-BLWVbuKP.js +1 -0
- package/dist/ui/assets/{AnnotatedAttachmentWorkspace-Dvi02Y7q.js → AnnotatedAttachmentWorkspace-BlS-cqnl.js} +1 -1
- package/dist/ui/assets/{DocumentWorkspace-iys3zfca.js → DocumentWorkspace-BH_6DF6x.js} +1 -1
- package/dist/ui/assets/{InitiativesModule-DJvLJI3A.js → InitiativesModule-BjR6iBf9.js} +1 -1
- package/dist/ui/assets/{PlansPage-BlVC_lRq.css → PlansPage-C2dd2n1X.css} +1 -1
- package/dist/ui/assets/PlansPage-CSDQ4sLa.js +1 -0
- package/dist/ui/assets/TaskSettings-CEDy34Jo.js +9 -0
- package/dist/ui/assets/{WorkflowsModule-BguHeQaV.js → WorkflowsModule-DnFqdUME.js} +1 -1
- package/dist/ui/assets/index-DXUdtH1B.js +6 -0
- package/dist/ui/assets/index-Ii0B0rTO.css +1 -0
- package/dist/ui/assets/{vendor-icons-C8EED3Oh.js → vendor-icons-I7ZwG1z_.js} +1 -1
- package/dist/ui/index.html +4 -4
- package/dist/utils/accountProfileSummaryCache.d.ts +31 -0
- package/dist/utils/accountProfileSummaryCache.js +97 -0
- package/dist/utils/accountProfileSummaryCache.test.d.ts +1 -0
- package/dist/utils/accountProfileSummaryCache.test.js +63 -0
- package/dist/utils/taskActivity.js +4 -0
- package/dist/utils/taskActivity.test.js +25 -1
- package/package.json +5 -3
- package/scripts/playwright-auth.sh +4 -0
- package/dist/ui/assets/AgentsModule-JG5jc3he.js +0 -1
- package/dist/ui/assets/PlansPage-BMPlpFpw.js +0 -1
- package/dist/ui/assets/TaskSettings-Bqd4cCzW.js +0 -8
- package/dist/ui/assets/index-MXeWoebC.css +0 -1
- package/dist/ui/assets/index-YJ4k5QNa.js +0 -6
- package/dist/utils/billingStatusCache.d.ts +0 -21
- package/dist/utils/billingStatusCache.js +0 -70
|
@@ -6,6 +6,7 @@ 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;
|
|
9
10
|
const BILLING_STATUS_RECONCILIATION_TTL_MS = 30_000;
|
|
10
11
|
const TASKFORCE_PROXY_ORIGIN_HOST_HEADER = 'x-taskforce-origin-host';
|
|
11
12
|
const TASKFORCE_PROXY_ORIGIN_PROTO_HEADER = 'x-taskforce-origin-proto';
|
|
@@ -26,6 +27,15 @@ function isoFromUnixSeconds(raw) {
|
|
|
26
27
|
return null;
|
|
27
28
|
return new Date(Math.floor(value) * 1000).toISOString();
|
|
28
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
|
+
}
|
|
29
39
|
function parseInterval(raw) {
|
|
30
40
|
const value = String(raw || '').trim().toLowerCase();
|
|
31
41
|
if (value === 'month' || value === 'year')
|
|
@@ -230,6 +240,8 @@ function mapStripeStatusToCanonicalState(statusRaw) {
|
|
|
230
240
|
return 'active';
|
|
231
241
|
if (status === 'past_due')
|
|
232
242
|
return 'grace';
|
|
243
|
+
if (status === 'incomplete')
|
|
244
|
+
return 'checkout_pending';
|
|
233
245
|
return 'canceled';
|
|
234
246
|
}
|
|
235
247
|
function isMissingStripeCustomerError(error) {
|
|
@@ -474,28 +486,264 @@ function getEntitlementRow(core, userIdRaw) {
|
|
|
474
486
|
const db = core.getDatabaseAdapter();
|
|
475
487
|
const tenantId = core.getTenantId();
|
|
476
488
|
const row = db.prepare(`
|
|
477
|
-
SELECT account_id, plan_version_id, entitlement_state
|
|
489
|
+
SELECT account_id, plan_version_id, entitlement_state, effective_until, updated_by
|
|
478
490
|
FROM account_entitlements
|
|
479
491
|
WHERE tenant_id = ? AND account_id = ?
|
|
480
492
|
LIMIT 1
|
|
481
493
|
`).get(tenantId, userId);
|
|
482
494
|
return row || null;
|
|
483
495
|
}
|
|
484
|
-
function
|
|
496
|
+
function updateCheckoutAttemptStatus(core, sessionIdRaw, status) {
|
|
497
|
+
const sessionId = String(sessionIdRaw || '').trim();
|
|
498
|
+
if (!sessionId)
|
|
499
|
+
return;
|
|
485
500
|
const db = core.getDatabaseAdapter();
|
|
486
501
|
const tenantId = core.getTenantId();
|
|
487
|
-
const now = new Date().toISOString();
|
|
488
502
|
db.prepare(`
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
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 updatedBy = input.updatedBy || 'checkout-start';
|
|
737
|
+
const source = input.source || 'checkout-start';
|
|
738
|
+
core.transitionCanonicalCommercialState({
|
|
739
|
+
accountId: input.userId,
|
|
740
|
+
planVersionId: input.planVersionId,
|
|
741
|
+
state: 'checkout_pending',
|
|
742
|
+
effectiveUntil: input.effectiveUntil || null,
|
|
743
|
+
billingMirrorPlanId: resolvePlanIdForVersion(core, input.planVersionId),
|
|
744
|
+
source,
|
|
745
|
+
updatedBy
|
|
746
|
+
});
|
|
499
747
|
}
|
|
500
748
|
function upsertBillingMirror(core, input) {
|
|
501
749
|
const db = core.getDatabaseAdapter();
|
|
@@ -546,7 +794,12 @@ function applySubscriptionSnapshot(core, input) {
|
|
|
546
794
|
const canceledState = stripeStatus === 'canceled'
|
|
547
795
|
? (currentPeriodEnd && currentPeriodEnd > nowIso ? 'active' : 'canceled')
|
|
548
796
|
: null;
|
|
549
|
-
const
|
|
797
|
+
const mappedCanonicalState = canceledState || mapStripeStatusToCanonicalState(stripeStatus);
|
|
798
|
+
const preserveExistingAccess = mappedCanonicalState === 'checkout_pending'
|
|
799
|
+
&& isCommercialStateAppAccessible(existingEntitlement?.entitlement_state);
|
|
800
|
+
const canonicalState = preserveExistingAccess
|
|
801
|
+
? String(existingEntitlement?.entitlement_state || 'active').trim().toLowerCase()
|
|
802
|
+
: mappedCanonicalState;
|
|
550
803
|
const effectiveUntil = stripeStatus === 'canceled'
|
|
551
804
|
? currentPeriodEnd
|
|
552
805
|
: (currentPeriodEnd || null);
|
|
@@ -563,13 +816,25 @@ function applySubscriptionSnapshot(core, input) {
|
|
|
563
816
|
currentPeriodEnd,
|
|
564
817
|
effectiveUntil
|
|
565
818
|
});
|
|
566
|
-
if (planVersionId) {
|
|
567
|
-
|
|
568
|
-
|
|
819
|
+
if (planVersionId && canonicalState === 'checkout_pending') {
|
|
820
|
+
if (shouldRecordCheckoutPendingState(existingEntitlement)) {
|
|
821
|
+
beginCheckoutPending(core, {
|
|
822
|
+
userId,
|
|
823
|
+
planVersionId,
|
|
824
|
+
source,
|
|
825
|
+
updatedBy: source
|
|
826
|
+
});
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
else if (planVersionId) {
|
|
830
|
+
core.transitionCanonicalCommercialState({
|
|
831
|
+
accountId: userId,
|
|
569
832
|
planVersionId: planVersionId || existingEntitlement?.plan_version_id || core.getDefaultPlanVersionIdByMode('personal', { allowFallback: true }),
|
|
570
833
|
state: canonicalState,
|
|
571
834
|
effectiveAt: trialEnd || nowIso,
|
|
572
835
|
effectiveUntil,
|
|
836
|
+
billingMirrorPlanId: resolvePlanIdForVersion(core, planVersionId),
|
|
837
|
+
source,
|
|
573
838
|
updatedBy: source
|
|
574
839
|
});
|
|
575
840
|
}
|
|
@@ -934,12 +1199,23 @@ export function registerBillingRoutes(deps) {
|
|
|
934
1199
|
return;
|
|
935
1200
|
}
|
|
936
1201
|
try {
|
|
1202
|
+
maintainCheckoutAttempts(core);
|
|
937
1203
|
const userId = requestUserId(req);
|
|
938
1204
|
const db = core.getDatabaseAdapter();
|
|
939
1205
|
const tenantId = core.getTenantId();
|
|
940
|
-
const
|
|
941
|
-
const row = billingReconciliation.row;
|
|
1206
|
+
const row = getBillingMirrorRow(core, userId);
|
|
942
1207
|
const access = core.getAccountAccessStatus(userId);
|
|
1208
|
+
const billingConflictCode = resolveBillingConflictCode(core, {
|
|
1209
|
+
userId,
|
|
1210
|
+
gate: String(access.gate || '')
|
|
1211
|
+
});
|
|
1212
|
+
const resolvedPlanId = String(access.entitlement?.planId || row?.tier || '').trim() || null;
|
|
1213
|
+
const planRow = resolvedPlanId ? db.prepare(`
|
|
1214
|
+
SELECT display_name
|
|
1215
|
+
FROM plan_catalog
|
|
1216
|
+
WHERE tenant_id = ? AND plan_id = ?
|
|
1217
|
+
LIMIT 1
|
|
1218
|
+
`).get(tenantId, resolvedPlanId) : undefined;
|
|
943
1219
|
if (!access.hasEntitlement || !access.entitlement) {
|
|
944
1220
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
945
1221
|
res.end(JSON.stringify({
|
|
@@ -951,24 +1227,18 @@ export function registerBillingRoutes(deps) {
|
|
|
951
1227
|
trialEnd: row?.trial_end || null,
|
|
952
1228
|
currentPeriodEnd: row?.current_period_end || null,
|
|
953
1229
|
effectiveUntil: row?.effective_until || null,
|
|
954
|
-
entitlementState: null,
|
|
1230
|
+
entitlementState: row?.entitlement_state || null,
|
|
955
1231
|
planVersionId: null,
|
|
956
|
-
planId:
|
|
957
|
-
planName:
|
|
1232
|
+
planId: resolvedPlanId,
|
|
1233
|
+
planName: planRow?.display_name ? String(planRow.display_name) : resolvedPlanId,
|
|
958
1234
|
gate: access.gate,
|
|
959
1235
|
message: access.message,
|
|
960
1236
|
planSelectionRequired: access.planSelectionRequired,
|
|
961
|
-
billingConflictCode
|
|
1237
|
+
billingConflictCode
|
|
962
1238
|
}));
|
|
963
1239
|
return;
|
|
964
1240
|
}
|
|
965
1241
|
const entitlement = access.entitlement;
|
|
966
|
-
const planRow = db.prepare(`
|
|
967
|
-
SELECT display_name
|
|
968
|
-
FROM plan_catalog
|
|
969
|
-
WHERE tenant_id = ? AND plan_id = ?
|
|
970
|
-
LIMIT 1
|
|
971
|
-
`).get(tenantId, entitlement.planId);
|
|
972
1242
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
973
1243
|
res.end(JSON.stringify({
|
|
974
1244
|
stripeStatus: row?.stripe_status || null,
|
|
@@ -986,7 +1256,7 @@ export function registerBillingRoutes(deps) {
|
|
|
986
1256
|
gate: access.gate,
|
|
987
1257
|
message: access.message,
|
|
988
1258
|
planSelectionRequired: access.planSelectionRequired,
|
|
989
|
-
billingConflictCode
|
|
1259
|
+
billingConflictCode
|
|
990
1260
|
}));
|
|
991
1261
|
}
|
|
992
1262
|
catch (error) {
|
|
@@ -1290,6 +1560,68 @@ export function registerBillingRoutes(deps) {
|
|
|
1290
1560
|
rows
|
|
1291
1561
|
}));
|
|
1292
1562
|
});
|
|
1563
|
+
addRoute('GET', '/api/taskforce/admin/billing/checkout-attempts', async (req, res) => {
|
|
1564
|
+
if (!ensureCloudBilling(req, res))
|
|
1565
|
+
return;
|
|
1566
|
+
if (!ensureAdminRole(req)) {
|
|
1567
|
+
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
1568
|
+
res.end(JSON.stringify({ error: 'Forbidden: system admin role required.' }));
|
|
1569
|
+
return;
|
|
1570
|
+
}
|
|
1571
|
+
const db = core.getDatabaseAdapter();
|
|
1572
|
+
const tenantId = core.getTenantId();
|
|
1573
|
+
const requestUrl = parseRequestUrl(req);
|
|
1574
|
+
const limit = parsePositiveIntBounded(requestUrl.searchParams.get('limit'), 50, 1, 200);
|
|
1575
|
+
const filterUserId = String(requestUrl.searchParams.get('userId') || '').trim();
|
|
1576
|
+
const now = new Date().toISOString();
|
|
1577
|
+
const maintenance = maintainCheckoutAttempts(core);
|
|
1578
|
+
const rows = filterUserId
|
|
1579
|
+
? db.prepare(`
|
|
1580
|
+
SELECT session_id, user_id, plan_version_id, billing_interval, status, expires_at, created_at, updated_at
|
|
1581
|
+
FROM billing_checkout_attempts
|
|
1582
|
+
WHERE tenant_id = ? AND user_id = ?
|
|
1583
|
+
ORDER BY created_at DESC, session_id DESC
|
|
1584
|
+
LIMIT ?
|
|
1585
|
+
`).all(tenantId, filterUserId, limit)
|
|
1586
|
+
: db.prepare(`
|
|
1587
|
+
SELECT session_id, user_id, plan_version_id, billing_interval, status, expires_at, created_at, updated_at
|
|
1588
|
+
FROM billing_checkout_attempts
|
|
1589
|
+
WHERE tenant_id = ?
|
|
1590
|
+
ORDER BY created_at DESC, session_id DESC
|
|
1591
|
+
LIMIT ?
|
|
1592
|
+
`).all(tenantId, limit);
|
|
1593
|
+
const total = filterUserId
|
|
1594
|
+
? db.prepare(`
|
|
1595
|
+
SELECT COUNT(*) AS count
|
|
1596
|
+
FROM billing_checkout_attempts
|
|
1597
|
+
WHERE tenant_id = ? AND user_id = ?
|
|
1598
|
+
`).get(tenantId, filterUserId)
|
|
1599
|
+
: db.prepare(`
|
|
1600
|
+
SELECT COUNT(*) AS count
|
|
1601
|
+
FROM billing_checkout_attempts
|
|
1602
|
+
WHERE tenant_id = ?
|
|
1603
|
+
`).get(tenantId);
|
|
1604
|
+
const openCount = filterUserId
|
|
1605
|
+
? db.prepare(`
|
|
1606
|
+
SELECT COUNT(*) AS count
|
|
1607
|
+
FROM billing_checkout_attempts
|
|
1608
|
+
WHERE tenant_id = ? AND user_id = ? AND status = 'open'
|
|
1609
|
+
`).get(tenantId, filterUserId)
|
|
1610
|
+
: db.prepare(`
|
|
1611
|
+
SELECT COUNT(*) AS count
|
|
1612
|
+
FROM billing_checkout_attempts
|
|
1613
|
+
WHERE tenant_id = ? AND status = 'open'
|
|
1614
|
+
`).get(tenantId);
|
|
1615
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1616
|
+
res.end(JSON.stringify({
|
|
1617
|
+
asOf: now,
|
|
1618
|
+
expiredRowsNormalized: maintenance.expiredRows,
|
|
1619
|
+
purgedTerminalRows: maintenance.purgedRows,
|
|
1620
|
+
total: Number(total?.count || 0),
|
|
1621
|
+
openCount: Number(openCount?.count || 0),
|
|
1622
|
+
rows
|
|
1623
|
+
}));
|
|
1624
|
+
});
|
|
1293
1625
|
addRoute('POST', '/api/taskforce/admin/billing/prices/import', async (req, res) => {
|
|
1294
1626
|
if (!ensureCloudBilling(req, res))
|
|
1295
1627
|
return;
|
|
@@ -1388,6 +1720,7 @@ export function registerBillingRoutes(deps) {
|
|
|
1388
1720
|
return;
|
|
1389
1721
|
}
|
|
1390
1722
|
try {
|
|
1723
|
+
maintainCheckoutAttempts(core);
|
|
1391
1724
|
const userId = requestUserId(req);
|
|
1392
1725
|
const currentBillingResult = await reconcileBillingMirrorIfNeeded(core, userId);
|
|
1393
1726
|
const currentBilling = currentBillingResult.row;
|
|
@@ -1551,6 +1884,21 @@ export function registerBillingRoutes(deps) {
|
|
|
1551
1884
|
billingInterval: interval
|
|
1552
1885
|
}
|
|
1553
1886
|
});
|
|
1887
|
+
const checkoutEffectiveUntil = isoFromUnixSeconds(session.expires_at);
|
|
1888
|
+
recordCheckoutAttempt(core, {
|
|
1889
|
+
sessionId: session.id,
|
|
1890
|
+
userId,
|
|
1891
|
+
planVersionId,
|
|
1892
|
+
billingInterval: interval,
|
|
1893
|
+
expiresAt: checkoutEffectiveUntil
|
|
1894
|
+
});
|
|
1895
|
+
if (shouldRecordCheckoutPendingState(currentEntitlement) && !isCommercialStateAppAccessible(currentEntitlement?.entitlement_state)) {
|
|
1896
|
+
beginCheckoutPending(core, {
|
|
1897
|
+
userId,
|
|
1898
|
+
planVersionId,
|
|
1899
|
+
effectiveUntil: checkoutEffectiveUntil
|
|
1900
|
+
});
|
|
1901
|
+
}
|
|
1554
1902
|
const checkoutUrl = isMockStripeClient(stripe)
|
|
1555
1903
|
? buildMockStripeHostedCheckoutUrl(resolveHostedServerBaseUrl(req), session.id)
|
|
1556
1904
|
: (session.url || null);
|
|
@@ -1592,11 +1940,30 @@ export function registerBillingRoutes(deps) {
|
|
|
1592
1940
|
return;
|
|
1593
1941
|
}
|
|
1594
1942
|
try {
|
|
1943
|
+
maintainCheckoutAttempts(core);
|
|
1595
1944
|
const stripe = getStripeClient();
|
|
1596
1945
|
const expectedUserId = requestUserId(req);
|
|
1597
1946
|
const session = await stripe.checkout.sessions.retrieve(sessionId, {
|
|
1598
1947
|
expand: ['subscription']
|
|
1599
1948
|
});
|
|
1949
|
+
const sessionStatus = String(session.status || '').trim().toLowerCase();
|
|
1950
|
+
const paymentStatus = String(session.payment_status || '').trim().toLowerCase();
|
|
1951
|
+
if (session.mode !== 'subscription') {
|
|
1952
|
+
res.writeHead(409, { 'Content-Type': 'application/json' });
|
|
1953
|
+
res.end(JSON.stringify({
|
|
1954
|
+
error: 'Checkout session is not a subscription checkout.',
|
|
1955
|
+
code: 'CHECKOUT_SESSION_INVALID_MODE'
|
|
1956
|
+
}));
|
|
1957
|
+
return;
|
|
1958
|
+
}
|
|
1959
|
+
if (sessionStatus !== 'complete' || (paymentStatus !== 'paid' && paymentStatus !== 'no_payment_required')) {
|
|
1960
|
+
res.writeHead(409, { 'Content-Type': 'application/json' });
|
|
1961
|
+
res.end(JSON.stringify({
|
|
1962
|
+
error: 'Checkout session has not completed yet.',
|
|
1963
|
+
code: 'CHECKOUT_SESSION_INCOMPLETE'
|
|
1964
|
+
}));
|
|
1965
|
+
return;
|
|
1966
|
+
}
|
|
1600
1967
|
const metadata = session.metadata || {};
|
|
1601
1968
|
const customerId = String(session.customer || '').trim();
|
|
1602
1969
|
const subscriptionId = typeof session.subscription === 'string'
|
|
@@ -1615,22 +1982,47 @@ export function registerBillingRoutes(deps) {
|
|
|
1615
1982
|
res.end(JSON.stringify({ error: 'Checkout session does not belong to the current account.' }));
|
|
1616
1983
|
return;
|
|
1617
1984
|
}
|
|
1985
|
+
let resolvedPlanVersionId = null;
|
|
1986
|
+
let subscription = null;
|
|
1987
|
+
if (subscriptionId) {
|
|
1988
|
+
subscription = typeof session.subscription === 'string' || !session.subscription
|
|
1989
|
+
? await stripe.subscriptions.retrieve(subscriptionId)
|
|
1990
|
+
: session.subscription;
|
|
1991
|
+
const priceId = String(subscription.items?.data?.[0]?.price?.id || '').trim();
|
|
1992
|
+
resolvedPlanVersionId = resolvePlanVersionIdForBillingEvent(core, [
|
|
1993
|
+
subscription.metadata?.planVersionId,
|
|
1994
|
+
subscription.metadata?.plan_version_id,
|
|
1995
|
+
metadata.planVersionId,
|
|
1996
|
+
metadata.plan_version_id
|
|
1997
|
+
], priceId).planVersionId;
|
|
1998
|
+
const lifecycleGuard = resolveCheckoutLifecycleGuard(core, {
|
|
1999
|
+
userId: resolvedUserId,
|
|
2000
|
+
planVersionId: resolvedPlanVersionId,
|
|
2001
|
+
sessionId,
|
|
2002
|
+
subscriptionId
|
|
2003
|
+
});
|
|
2004
|
+
if (lifecycleGuard.blocked) {
|
|
2005
|
+
res.writeHead(409, { 'Content-Type': 'application/json' });
|
|
2006
|
+
res.end(JSON.stringify({
|
|
2007
|
+
error: lifecycleGuard.message,
|
|
2008
|
+
code: lifecycleGuard.code
|
|
2009
|
+
}));
|
|
2010
|
+
return;
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
1618
2013
|
core.createUserBillingRecord(resolvedUserId);
|
|
1619
2014
|
upsertBillingMirror(core, {
|
|
1620
2015
|
userId: resolvedUserId,
|
|
1621
2016
|
stripeCustomerId: customerId || null,
|
|
1622
2017
|
stripeSubscriptionId: subscriptionId || null
|
|
1623
2018
|
});
|
|
1624
|
-
|
|
1625
|
-
if (session.mode === 'subscription' && subscriptionId) {
|
|
1626
|
-
const subscription = typeof session.subscription === 'string' || !session.subscription
|
|
1627
|
-
? await stripe.subscriptions.retrieve(subscriptionId)
|
|
1628
|
-
: session.subscription;
|
|
2019
|
+
if (subscription) {
|
|
1629
2020
|
resolvedPlanVersionId = applySubscriptionSnapshot(core, {
|
|
1630
2021
|
userId: resolvedUserId,
|
|
1631
2022
|
subscription,
|
|
1632
2023
|
source: 'checkout-confirm'
|
|
1633
2024
|
}).planVersionId;
|
|
2025
|
+
updateCheckoutAttemptStatus(core, sessionId, 'confirmed');
|
|
1634
2026
|
}
|
|
1635
2027
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1636
2028
|
res.end(JSON.stringify({
|
|
@@ -1801,6 +2193,7 @@ export function registerBillingRoutes(deps) {
|
|
|
1801
2193
|
const db = core.getDatabaseAdapter();
|
|
1802
2194
|
const tenantId = core.getTenantId();
|
|
1803
2195
|
purgeExpiredWebhookFailures(core);
|
|
2196
|
+
maintainCheckoutAttempts(core);
|
|
1804
2197
|
let duplicate = false;
|
|
1805
2198
|
const tx = db.transaction(() => {
|
|
1806
2199
|
const acquired = lockWebhookEvent(core, event);
|
|
@@ -1853,11 +2246,31 @@ export function registerBillingRoutes(deps) {
|
|
|
1853
2246
|
});
|
|
1854
2247
|
return;
|
|
1855
2248
|
}
|
|
1856
|
-
|
|
2249
|
+
const lifecycleGuard = resolveCheckoutLifecycleGuard(core, {
|
|
2250
|
+
userId,
|
|
2251
|
+
planVersionId,
|
|
2252
|
+
subscriptionId: String(subscription.id || '').trim()
|
|
2253
|
+
});
|
|
2254
|
+
if (lifecycleGuard.blocked) {
|
|
2255
|
+
recordWebhookFailure(core, {
|
|
2256
|
+
eventId: event.id,
|
|
2257
|
+
eventType: event.type,
|
|
2258
|
+
reason: lifecycleGuard.message,
|
|
2259
|
+
payload: event
|
|
2260
|
+
});
|
|
2261
|
+
return;
|
|
2262
|
+
}
|
|
2263
|
+
const snapshot = applySubscriptionSnapshot(core, {
|
|
1857
2264
|
userId,
|
|
1858
2265
|
subscription,
|
|
1859
2266
|
source: 'stripe-webhook'
|
|
1860
2267
|
});
|
|
2268
|
+
if (snapshot.canonicalState !== 'checkout_pending') {
|
|
2269
|
+
markLatestOpenCheckoutAttemptConfirmed(core, {
|
|
2270
|
+
userId,
|
|
2271
|
+
planVersionId: snapshot.planVersionId
|
|
2272
|
+
});
|
|
2273
|
+
}
|
|
1861
2274
|
return;
|
|
1862
2275
|
}
|
|
1863
2276
|
if (event.type === 'invoice.paid' || event.type === 'invoice.payment_failed') {
|
|
@@ -1878,6 +2291,20 @@ export function registerBillingRoutes(deps) {
|
|
|
1878
2291
|
invoice.subscription_details?.metadata?.plan_version_id
|
|
1879
2292
|
], priceId);
|
|
1880
2293
|
const planVersionId = planVersionResolution.planVersionId;
|
|
2294
|
+
const lifecycleGuard = resolveCheckoutLifecycleGuard(core, {
|
|
2295
|
+
userId,
|
|
2296
|
+
planVersionId,
|
|
2297
|
+
subscriptionId
|
|
2298
|
+
});
|
|
2299
|
+
if (lifecycleGuard.blocked) {
|
|
2300
|
+
recordWebhookFailure(core, {
|
|
2301
|
+
eventId: event.id,
|
|
2302
|
+
eventType: event.type,
|
|
2303
|
+
reason: lifecycleGuard.message,
|
|
2304
|
+
payload: event
|
|
2305
|
+
});
|
|
2306
|
+
return;
|
|
2307
|
+
}
|
|
1881
2308
|
const existingEntitlement = getEntitlementRow(core, userId);
|
|
1882
2309
|
const canonicalState = event.type === 'invoice.payment_failed' ? 'grace' : 'active';
|
|
1883
2310
|
const currentPeriodEnd = invoice.lines?.data?.[0]?.period?.end
|
|
@@ -1906,12 +2333,19 @@ export function registerBillingRoutes(deps) {
|
|
|
1906
2333
|
});
|
|
1907
2334
|
return;
|
|
1908
2335
|
}
|
|
1909
|
-
|
|
1910
|
-
userId,
|
|
2336
|
+
core.transitionCanonicalCommercialState({
|
|
2337
|
+
accountId: userId,
|
|
1911
2338
|
planVersionId: planVersionId || existingEntitlement?.plan_version_id || core.getDefaultPlanVersionIdByMode('personal', { allowFallback: true }),
|
|
1912
2339
|
state: canonicalState,
|
|
1913
2340
|
effectiveAt: new Date().toISOString(),
|
|
1914
|
-
effectiveUntil: currentPeriodEnd
|
|
2341
|
+
effectiveUntil: currentPeriodEnd,
|
|
2342
|
+
billingMirrorPlanId: resolvePlanIdForVersion(core, planVersionId),
|
|
2343
|
+
source: 'stripe-webhook',
|
|
2344
|
+
updatedBy: 'stripe-webhook'
|
|
2345
|
+
});
|
|
2346
|
+
markLatestOpenCheckoutAttemptConfirmed(core, {
|
|
2347
|
+
userId,
|
|
2348
|
+
planVersionId
|
|
1915
2349
|
});
|
|
1916
2350
|
return;
|
|
1917
2351
|
}
|