@taskforcehq/taskforce 0.3.303 → 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.
- package/README.md +140 -247
- package/dist/Taskforce.module.css +102 -5
- package/dist/TaskforceCore.js +635 -238
- package/dist/TaskforceCore.test.js +2243 -495
- package/dist/annotatedAttachments/service.d.ts +1 -1
- package/dist/annotatedAttachments/types.d.ts +1 -1
- package/dist/compat/workspaceSyncCompat.js +6 -0
- package/dist/components/features/AgentsModule.js +1 -1
- package/dist/components/features/AgentsModule.test.js +1 -1
- package/dist/components/features/DocumentIndex.d.ts +1 -1
- package/dist/components/features/DocumentIndex.js +1 -1
- package/dist/components/features/DocumentViewer.d.ts +1 -1
- package/dist/components/features/DocumentViewer.js +32 -9
- package/dist/components/features/DocumentWorkspace.d.ts +5 -22
- package/dist/components/features/DocumentWorkspace.js +12 -155
- package/dist/components/features/DocumentWorkspace.test.js +193 -6
- package/dist/components/features/TaskSettings.js +87 -41
- package/dist/components/features/TaskSettings.test.js +180 -40
- package/dist/components/features/documentWorkspaceModel.d.ts +24 -0
- package/dist/components/features/documentWorkspaceModel.js +57 -0
- package/dist/components/features/useDocumentWorkspaceController.d.ts +34 -0
- package/dist/components/features/useDocumentWorkspaceController.js +113 -0
- 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 +9 -8
- package/dist/components/task/TaskForm.js +374 -219
- package/dist/components/task/TaskForm.test.js +482 -361
- package/dist/components/task/TaskKanban.d.ts +1 -1
- package/dist/components/task/TaskKanban.js +33 -161
- 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 +9 -10
- package/dist/components/views/PlanComparisonPage.test.js +55 -41
- package/dist/components/views/PlansPage.d.ts +5 -1
- package/dist/components/views/PlansPage.js +150 -81
- package/dist/components/views/PlansPage.test.js +367 -41
- package/dist/components/views/StandaloneLayout.js +135 -54
- package/dist/components/views/WidgetView.js +11 -2
- package/dist/components/views/panels/FilterToolbar.test.js +6 -4
- package/dist/components/views/standalone/modals/AccountHubModal.d.ts +2 -14
- package/dist/components/views/standalone/modals/AccountHubModal.js +12 -14
- package/dist/components/views/standalone/modals/AccountHubModal.test.js +24 -1
- package/dist/components/views/standalone/modals/SyncStatusModal.js +1 -1
- package/dist/config/envSchema.js +1 -1
- package/dist/core/AiProfileService.d.ts +6 -1
- package/dist/core/AiProfileService.js +161 -16
- package/dist/core/AiProfileService.test.js +3 -1
- package/dist/core/AiProfiles.test.js +200 -0
- package/dist/core/AuthTokenService.test.d.ts +1 -0
- package/dist/core/AuthTokenService.test.js +78 -0
- package/dist/core/DeletedTaskLifecycleService.d.ts +2 -1
- package/dist/core/EntitlementsPolicy.test.js +75 -13
- package/dist/core/PlanEntitlementService.d.ts +9 -1
- package/dist/core/PlanEntitlementService.js +144 -19
- package/dist/core/PlanVersionPolicy.test.js +19 -26
- package/dist/core/SignupMonetizationSettings.test.js +75 -15
- package/dist/core/SystemAdmin.test.js +212 -56
- 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/TaskTaxonomyValidation.test.js +53 -0
- package/dist/core/Taskforce.d.ts +18 -11
- package/dist/core/Taskforce.js +93 -12
- package/dist/core/WorkspacePlanMode.test.js +73 -8
- package/dist/core/shared.d.ts +2 -0
- package/dist/core/shared.js +11 -0
- 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 +63 -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 +123 -0
- package/dist/hooks/sync/transfers.js +435 -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 +12 -8
- package/dist/hooks/useSyncOrchestrator.js +388 -879
- package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +641 -6
- package/dist/hooks/useTaskMutations.d.ts +2 -1
- package/dist/hooks/useTaskMutations.js +57 -7
- package/dist/hooks/useTaskMutations.test.js +176 -0
- package/dist/hooks/useTaskforce.d.ts +13 -4
- package/dist/hooks/useTaskforce.js +302 -817
- package/dist/hooks/useTaskforce.sync-behavior.test.js +78 -0
- package/dist/hooks/useWorkspaceSyncController.d.ts +4 -1
- package/dist/hooks/useWorkspaceSyncController.js +33 -11
- package/dist/hooks/useWorkspaceSyncController.test.js +1 -0
- 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/adminWorkspaceRegistrar.d.ts +2 -0
- package/dist/mcp/adminWorkspaceRegistrar.js +154 -0
- 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/collaborationRegistrar.d.ts +2 -0
- package/dist/mcp/collaborationRegistrar.js +71 -0
- package/dist/mcp/documentAssetRegistrar.d.ts +2 -0
- package/dist/mcp/documentAssetRegistrar.js +346 -0
- package/dist/mcp/documentHelpers.d.ts +100 -0
- package/dist/mcp/documentHelpers.js +161 -0
- package/dist/mcp/runtime.js +2455 -3925
- package/dist/mcp/runtime.test.js +240 -8
- 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/mcp/taskPlanningRegistrar.d.ts +2 -0
- package/dist/mcp/taskPlanningRegistrar.js +418 -0
- package/dist/mcp/toolCatalog.d.ts +35 -0
- package/dist/mcp/toolCatalog.js +3 -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.d.ts +1 -0
- package/dist/server/index.js +46 -3
- package/dist/server/index.test.js +12 -1
- package/dist/server/routes/admin.d.ts +1 -5
- package/dist/server/routes/admin.js +151 -80
- package/dist/server/routes/annotatedAttachments.d.ts +1 -1
- package/dist/server/routes/annotatedAttachments.js +1 -1
- package/dist/server/routes/auth.d.ts +1 -4
- package/dist/server/routes/auth.js +59 -64
- package/dist/server/routes/authSupport.d.ts +30 -0
- package/dist/server/routes/authSupport.js +81 -0
- package/dist/server/routes/billing.d.ts +1 -1
- package/dist/server/routes/billing.js +122 -255
- package/dist/server/routes/billing.test.js +257 -86
- package/dist/server/routes/documentReviews.d.ts +1 -1
- package/dist/server/routes/documentReviews.js +1 -1
- package/dist/server/routes/documents.d.ts +4 -2
- package/dist/server/routes/documents.js +73 -7
- package/dist/server/routes/primitives.d.ts +11 -0
- package/dist/server/routes/primitives.js +73 -0
- package/dist/server/routes/resources.d.ts +1 -1
- package/dist/server/routes/resources.js +1 -1
- package/dist/server/routes/shared.d.ts +31 -1
- package/dist/server/routes/shared.js +26 -0
- package/dist/server/routes/sync.d.ts +1 -1
- package/dist/server/routes/sync.integration.test.js +20 -0
- package/dist/server/routes/sync.js +111 -238
- package/dist/server/routes/tasks.d.ts +1 -1
- package/dist/server/routes/tasks.js +50 -8
- package/dist/server/routes/workspaces.d.ts +1 -1
- package/dist/server/routes/workspaces.js +1 -1
- package/dist/server/routes.d.ts +2 -9
- package/dist/server/routes.js +84 -181
- package/dist/server/routes.test.js +912 -32
- package/dist/shared/taskActor.d.ts +10 -0
- package/dist/shared/taskActor.js +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.js +28 -17
- 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 +379 -0
- package/dist/sync/syncService.d.ts +9 -1
- package/dist/sync/syncService.js +44 -2
- package/dist/sync/syncService.test.js +62 -0
- package/dist/sync/workspacePullFeed.d.ts +1 -0
- package/dist/sync/workspacePullFeed.js +66 -59
- package/dist/sync/workspacePullFeed.test.js +72 -4
- package/dist/sync/workspaceSyncModel.d.ts +25 -1
- package/dist/sync/workspaceSyncModel.js +77 -3
- package/dist/sync/workspaceSyncModel.test.js +153 -0
- package/dist/sync/workspaceSyncState.d.ts +2 -0
- package/dist/sync/workspaceSyncState.js +1 -0
- package/dist/types.d.ts +2 -10
- package/dist/ui/assets/AgentsModule-N2MnQBZk.js +1 -0
- package/dist/ui/assets/{AnnotatedAttachmentWorkspace-C_TmXZwA.js → AnnotatedAttachmentWorkspace-BG6P_GVW.js} +1 -1
- package/dist/ui/assets/DocumentWorkspace-B3nV_Gc5.css +1 -0
- package/dist/ui/assets/DocumentWorkspace-DRbDMwh8.js +252 -0
- package/dist/ui/assets/{InitiativesModule-4-Rh2K2n.js → InitiativesModule-Dhm7M8e7.js} +1 -1
- package/dist/ui/assets/{PlansPage-BlVC_lRq.css → PlansPage-C2dd2n1X.css} +1 -1
- package/dist/ui/assets/PlansPage-Cq0zXj3I.js +1 -0
- package/dist/ui/assets/TaskSettings-ln0aF7xc.js +9 -0
- package/dist/ui/assets/{WorkflowsModule-CGk9s3NJ.js → WorkflowsModule-BcS4afRn.js} +1 -1
- package/dist/ui/assets/index-C2-OXZV7.css +1 -0
- package/dist/ui/assets/index-DcSI5F86.js +6 -0
- package/dist/ui/assets/{vendor-icons-pWocEipT.js → vendor-icons-BSUaRwF8.js} +1 -1
- package/dist/ui/index.html +4 -4
- package/dist/utils/commercialLifecyclePresentation.d.ts +19 -0
- package/dist/utils/commercialLifecyclePresentation.js +196 -0
- package/dist/utils/taskActivity.js +4 -0
- package/dist/utils/taskActivity.test.js +25 -1
- package/package.json +10 -1
- package/scripts/check-cloud-mcp.mjs +83 -0
- package/scripts/playwright-auth.sh +9 -1
- package/scripts/run-billing-performance-smoke.sh +24 -0
- package/dist/ui/assets/AgentsModule-CpsTmrIW.js +0 -1
- package/dist/ui/assets/DocumentWorkspace-C3GyzrWm.js +0 -250
- package/dist/ui/assets/DocumentWorkspace-C_8T8oz-.css +0 -1
- package/dist/ui/assets/PlansPage-BP7AYOqr.js +0 -1
- package/dist/ui/assets/TaskSettings-BOC5F6Ag.js +0 -8
- package/dist/ui/assets/index-CLIMgR6g.js +0 -6
- package/dist/ui/assets/index-DneETxcu.css +0 -1
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { resolveEffectiveEmailConfig } from '../email.js';
|
|
2
|
+
import { resolveDeploymentConfig } from '../../config/deployment.js';
|
|
3
|
+
export function resolveBooleanSetting(value, fallback) {
|
|
4
|
+
if (typeof value === 'boolean')
|
|
5
|
+
return value;
|
|
6
|
+
if (typeof value === 'number')
|
|
7
|
+
return value !== 0;
|
|
8
|
+
const normalized = String(value || '').trim().toLowerCase();
|
|
9
|
+
if (!normalized)
|
|
10
|
+
return fallback;
|
|
11
|
+
if (['1', 'true', 'yes', 'on'].includes(normalized))
|
|
12
|
+
return true;
|
|
13
|
+
if (['0', 'false', 'no', 'off'].includes(normalized))
|
|
14
|
+
return false;
|
|
15
|
+
return fallback;
|
|
16
|
+
}
|
|
17
|
+
export function getRouteEmailConfig(core, context) {
|
|
18
|
+
if (context.authConfig?.runtimeMode !== 'cloud') {
|
|
19
|
+
return resolveEffectiveEmailConfig(process.env, { provider: 'console' });
|
|
20
|
+
}
|
|
21
|
+
const globalSettings = core.getGlobalSettings();
|
|
22
|
+
return resolveEffectiveEmailConfig(process.env, globalSettings.email || null);
|
|
23
|
+
}
|
|
24
|
+
export function buildAuthFlowBehavior(core) {
|
|
25
|
+
const globalSettings = core.getGlobalSettings();
|
|
26
|
+
const authSettings = globalSettings.auth || {};
|
|
27
|
+
const authRateLimitMaxRequestsRaw = Number(authSettings.authRateLimitMaxRequests);
|
|
28
|
+
const authRateLimitWindowMsRaw = Number(authSettings.authRateLimitWindowMs);
|
|
29
|
+
return {
|
|
30
|
+
allowSelfRegistration: resolveBooleanSetting(authSettings.allowSelfRegistration, true),
|
|
31
|
+
requireVerifiedEmail: resolveBooleanSetting(authSettings.requireVerifiedEmail, true),
|
|
32
|
+
defaultNewUserBetaAccess: resolveBooleanSetting(authSettings.defaultNewUserBetaAccess, false),
|
|
33
|
+
authRateLimitEnabled: resolveBooleanSetting(authSettings.authRateLimitEnabled, true),
|
|
34
|
+
authRateLimitMaxRequests: Number.isFinite(authRateLimitMaxRequestsRaw) && authRateLimitMaxRequestsRaw >= 1
|
|
35
|
+
? Math.floor(authRateLimitMaxRequestsRaw)
|
|
36
|
+
: 10,
|
|
37
|
+
authRateLimitWindowMs: Number.isFinite(authRateLimitWindowMsRaw) && authRateLimitWindowMsRaw >= 1000
|
|
38
|
+
? Math.floor(authRateLimitWindowMsRaw)
|
|
39
|
+
: 15 * 60_000,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
export function buildAuthBehavior(core) {
|
|
43
|
+
const authBehavior = buildAuthFlowBehavior(core);
|
|
44
|
+
const onboardingPolicy = core.getOnboardingPolicy();
|
|
45
|
+
return {
|
|
46
|
+
...authBehavior,
|
|
47
|
+
onboardingPolicyMode: onboardingPolicy.mode,
|
|
48
|
+
onboardingPlanVersionId: onboardingPolicy.planVersionId
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
export function buildSiteBehavior(core) {
|
|
52
|
+
const globalSettings = core.getGlobalSettings();
|
|
53
|
+
const siteSettings = globalSettings.site || {};
|
|
54
|
+
return {
|
|
55
|
+
showRunTaskforceLocally: resolveBooleanSetting(siteSettings.showRunTaskforceLocally, true),
|
|
56
|
+
pricingPageEnabled: resolveBooleanSetting(siteSettings.pricingPageEnabled, true)
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
export function shouldExposeDevAuthTokens() {
|
|
60
|
+
const explicit = String(process.env.TASKFORCE_AUTH_EXPOSE_DEV_TOKENS || '').trim().toLowerCase();
|
|
61
|
+
return explicit === '1' || explicit === 'true' || explicit === 'yes';
|
|
62
|
+
}
|
|
63
|
+
export function resolveRuntimeConfigBaseUrl(req) {
|
|
64
|
+
const host = String(req.headers['x-forwarded-host'] || req.headers.host || '').trim();
|
|
65
|
+
if (!host)
|
|
66
|
+
return '';
|
|
67
|
+
const proto = String(req.headers['x-forwarded-proto'] || '').trim().toLowerCase() === 'https'
|
|
68
|
+
? 'https'
|
|
69
|
+
: (req.socket?.encrypted ? 'https' : 'http');
|
|
70
|
+
return `${proto}://${host}`.replace(/\/+$/, '');
|
|
71
|
+
}
|
|
72
|
+
export function resolvePublicAuthBaseUrl(req) {
|
|
73
|
+
const configured = String(process.env.TASKFORCE_AUTH_PUBLIC_BASE_URL || '').trim().replace(/\/+$/, '');
|
|
74
|
+
if (configured)
|
|
75
|
+
return configured;
|
|
76
|
+
const host = String(req.headers['x-forwarded-host'] || req.headers.host || '').trim();
|
|
77
|
+
const proto = String(req.headers['x-forwarded-proto'] || '').trim().toLowerCase() === 'http' ? 'http' : 'https';
|
|
78
|
+
if (!host)
|
|
79
|
+
return resolveDeploymentConfig(process.env).cloudBaseUrl || 'https://app.taskforcehq.ai';
|
|
80
|
+
return `${proto}://${host}`;
|
|
81
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { IncomingMessage } from 'http';
|
|
2
2
|
import type { TaskforceCore } from '../../core/Taskforce.js';
|
|
3
|
-
import type
|
|
3
|
+
import { type RouteHandler, type RouteContext } from './primitives.js';
|
|
4
4
|
export type BillingInterval = 'month' | 'year';
|
|
5
5
|
export type BillingPricingType = 'stripe' | 'free';
|
|
6
6
|
export interface BillingRouteDependencies {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import Stripe from 'stripe';
|
|
2
|
-
import { parseJsonBody } from '
|
|
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';
|
|
@@ -733,72 +733,18 @@ function shouldRecordCheckoutPendingState(entitlement) {
|
|
|
733
733
|
|| currentState === 'canceled';
|
|
734
734
|
}
|
|
735
735
|
function beginCheckoutPending(core, input) {
|
|
736
|
-
const planId = resolvePlanIdForVersion(core, input.planVersionId);
|
|
737
736
|
const updatedBy = input.updatedBy || 'checkout-start';
|
|
738
737
|
const source = input.source || 'checkout-start';
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
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
|
|
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
|
|
770
746
|
});
|
|
771
747
|
}
|
|
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
|
-
}
|
|
787
|
-
const db = core.getDatabaseAdapter();
|
|
788
|
-
const tenantId = core.getTenantId();
|
|
789
|
-
const now = new Date().toISOString();
|
|
790
|
-
db.prepare(`
|
|
791
|
-
INSERT INTO account_entitlements (account_id, tenant_id, plan_version_id, entitlement_state, effective_at, effective_until, updated_by, created_at, updated_at)
|
|
792
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
793
|
-
ON CONFLICT (account_id) DO UPDATE SET
|
|
794
|
-
plan_version_id = excluded.plan_version_id,
|
|
795
|
-
entitlement_state = excluded.entitlement_state,
|
|
796
|
-
effective_at = excluded.effective_at,
|
|
797
|
-
effective_until = excluded.effective_until,
|
|
798
|
-
updated_by = excluded.updated_by,
|
|
799
|
-
updated_at = excluded.updated_at
|
|
800
|
-
`).run(input.userId, tenantId, input.planVersionId, input.state, input.effectiveAt || now, input.effectiveUntil || null, updatedBy, now, now);
|
|
801
|
-
}
|
|
802
748
|
function upsertBillingMirror(core, input) {
|
|
803
749
|
const db = core.getDatabaseAdapter();
|
|
804
750
|
const tenantId = core.getTenantId();
|
|
@@ -849,11 +795,15 @@ function applySubscriptionSnapshot(core, input) {
|
|
|
849
795
|
? (currentPeriodEnd && currentPeriodEnd > nowIso ? 'active' : 'canceled')
|
|
850
796
|
: null;
|
|
851
797
|
const mappedCanonicalState = canceledState || mapStripeStatusToCanonicalState(stripeStatus);
|
|
798
|
+
const existingPlanVersionId = String(existingEntitlement?.plan_version_id || '').trim();
|
|
852
799
|
const preserveExistingAccess = mappedCanonicalState === 'checkout_pending'
|
|
800
|
+
&& Boolean(planVersionId)
|
|
801
|
+
&& existingPlanVersionId === planVersionId
|
|
853
802
|
&& isCommercialStateAppAccessible(existingEntitlement?.entitlement_state);
|
|
854
803
|
const canonicalState = preserveExistingAccess
|
|
855
804
|
? String(existingEntitlement?.entitlement_state || 'active').trim().toLowerCase()
|
|
856
805
|
: mappedCanonicalState;
|
|
806
|
+
const billingConfirmedSource = source === 'stripe-webhook' || source === 'checkout-confirm';
|
|
857
807
|
const effectiveUntil = stripeStatus === 'canceled'
|
|
858
808
|
? currentPeriodEnd
|
|
859
809
|
: (currentPeriodEnd || null);
|
|
@@ -880,13 +830,15 @@ function applySubscriptionSnapshot(core, input) {
|
|
|
880
830
|
});
|
|
881
831
|
}
|
|
882
832
|
}
|
|
883
|
-
else if (planVersionId) {
|
|
884
|
-
|
|
885
|
-
userId,
|
|
833
|
+
else if (planVersionId && billingConfirmedSource) {
|
|
834
|
+
core.transitionCanonicalCommercialState({
|
|
835
|
+
accountId: userId,
|
|
886
836
|
planVersionId: planVersionId || existingEntitlement?.plan_version_id || core.getDefaultPlanVersionIdByMode('personal', { allowFallback: true }),
|
|
887
837
|
state: canonicalState,
|
|
888
838
|
effectiveAt: trialEnd || nowIso,
|
|
889
839
|
effectiveUntil,
|
|
840
|
+
billingMirrorPlanId: resolvePlanIdForVersion(core, planVersionId),
|
|
841
|
+
source,
|
|
890
842
|
updatedBy: source
|
|
891
843
|
});
|
|
892
844
|
}
|
|
@@ -1378,18 +1330,11 @@ export function registerBillingRoutes(deps) {
|
|
|
1378
1330
|
res.end(JSON.stringify({ error: 'Forbidden: system admin role required.' }));
|
|
1379
1331
|
return;
|
|
1380
1332
|
}
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
res.end(JSON.stringify({ options }));
|
|
1387
|
-
}
|
|
1388
|
-
catch (error) {
|
|
1389
|
-
const statusCode = resolveErrorStatusCode(error, 400);
|
|
1390
|
-
res.writeHead(statusCode, { 'Content-Type': 'application/json' });
|
|
1391
|
-
res.end(JSON.stringify({ error: String(error?.message || 'Unable to list Stripe prices'), code: error?.code }));
|
|
1392
|
-
}
|
|
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
|
+
}));
|
|
1393
1338
|
});
|
|
1394
1339
|
addRoute('PUT', '/api/taskforce/admin/billing/price-mappings', async (req, res) => {
|
|
1395
1340
|
if (!ensureCloudBilling(req, res))
|
|
@@ -1399,121 +1344,11 @@ export function registerBillingRoutes(deps) {
|
|
|
1399
1344
|
res.end(JSON.stringify({ error: 'Forbidden: system admin role required.' }));
|
|
1400
1345
|
return;
|
|
1401
1346
|
}
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
const setActive = body?.active === false ? false : true;
|
|
1408
|
-
if (!planVersionId || !interval) {
|
|
1409
|
-
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
1410
|
-
res.end(JSON.stringify({ error: 'planVersionId and interval are required.', code: 'BILLING_MAPPING_INPUT_REQUIRED' }));
|
|
1411
|
-
return;
|
|
1412
|
-
}
|
|
1413
|
-
if (pricingType === 'stripe' && !stripePriceId) {
|
|
1414
|
-
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
1415
|
-
res.end(JSON.stringify({ error: 'stripePriceId is required for Stripe pricing.', code: 'BILLING_MAPPING_STRIPE_PRICE_REQUIRED' }));
|
|
1416
|
-
return;
|
|
1417
|
-
}
|
|
1418
|
-
const db = core.getDatabaseAdapter();
|
|
1419
|
-
const tenantId = core.getTenantId();
|
|
1420
|
-
const versionExists = db.prepare(`
|
|
1421
|
-
SELECT plan_version_id
|
|
1422
|
-
FROM plan_versions
|
|
1423
|
-
WHERE tenant_id = ? AND plan_version_id = ?
|
|
1424
|
-
LIMIT 1
|
|
1425
|
-
`).get(tenantId, planVersionId);
|
|
1426
|
-
if (!versionExists?.plan_version_id) {
|
|
1427
|
-
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
1428
|
-
res.end(JSON.stringify({ error: 'Plan version not found.', code: 'PLAN_VERSION_NOT_FOUND' }));
|
|
1429
|
-
return;
|
|
1430
|
-
}
|
|
1431
|
-
try {
|
|
1432
|
-
const now = new Date().toISOString();
|
|
1433
|
-
const targetStripePriceId = pricingType === 'free'
|
|
1434
|
-
? buildFreePricingSyntheticId(planVersionId, interval)
|
|
1435
|
-
: stripePriceId;
|
|
1436
|
-
let unitAmount = 0;
|
|
1437
|
-
let currency = null;
|
|
1438
|
-
if (pricingType === 'stripe') {
|
|
1439
|
-
const stripe = getStripeClient();
|
|
1440
|
-
const stripePrice = await stripe.prices.retrieve(stripePriceId);
|
|
1441
|
-
const stripeInterval = parseInterval(stripePrice.recurring?.interval || '');
|
|
1442
|
-
if (!stripePrice.active) {
|
|
1443
|
-
res.writeHead(422, { 'Content-Type': 'application/json' });
|
|
1444
|
-
res.end(JSON.stringify({ error: 'Stripe price is not active.', code: 'STRIPE_PRICE_INACTIVE' }));
|
|
1445
|
-
return;
|
|
1446
|
-
}
|
|
1447
|
-
if (!stripeInterval) {
|
|
1448
|
-
res.writeHead(422, { 'Content-Type': 'application/json' });
|
|
1449
|
-
res.end(JSON.stringify({ error: 'Stripe price must be recurring month/year.', code: 'STRIPE_PRICE_INTERVAL_UNSUPPORTED' }));
|
|
1450
|
-
return;
|
|
1451
|
-
}
|
|
1452
|
-
if (stripeInterval !== interval) {
|
|
1453
|
-
res.writeHead(422, { 'Content-Type': 'application/json' });
|
|
1454
|
-
res.end(JSON.stringify({ error: `Stripe price interval (${stripeInterval}) does not match requested interval (${interval}).`, code: 'STRIPE_PRICE_INTERVAL_MISMATCH' }));
|
|
1455
|
-
return;
|
|
1456
|
-
}
|
|
1457
|
-
unitAmount = typeof stripePrice.unit_amount === 'number' ? stripePrice.unit_amount : null;
|
|
1458
|
-
currency = stripePrice.currency || null;
|
|
1459
|
-
}
|
|
1460
|
-
const beforeRows = db.prepare(`
|
|
1461
|
-
SELECT plan_version_id, billing_interval, stripe_price_id, pricing_type, active, unit_amount, currency
|
|
1462
|
-
FROM billing_price_mappings
|
|
1463
|
-
WHERE tenant_id = ? AND plan_version_id = ? AND billing_interval = ?
|
|
1464
|
-
ORDER BY stripe_price_id ASC
|
|
1465
|
-
`).all(tenantId, planVersionId, interval);
|
|
1466
|
-
if (setActive) {
|
|
1467
|
-
db.prepare(`
|
|
1468
|
-
UPDATE billing_price_mappings
|
|
1469
|
-
SET active = 0, updated_at = ?
|
|
1470
|
-
WHERE tenant_id = ?
|
|
1471
|
-
AND plan_version_id = ?
|
|
1472
|
-
AND billing_interval = ?
|
|
1473
|
-
AND stripe_price_id <> ?
|
|
1474
|
-
AND active = 1
|
|
1475
|
-
`).run(now, tenantId, planVersionId, interval, targetStripePriceId);
|
|
1476
|
-
}
|
|
1477
|
-
db.prepare(`
|
|
1478
|
-
INSERT INTO billing_price_mappings (
|
|
1479
|
-
tenant_id, plan_version_id, billing_interval, stripe_price_id, pricing_type, active, unit_amount, currency, created_at, updated_at
|
|
1480
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1481
|
-
ON CONFLICT (tenant_id, plan_version_id, billing_interval, stripe_price_id) DO UPDATE SET
|
|
1482
|
-
pricing_type = excluded.pricing_type,
|
|
1483
|
-
active = excluded.active,
|
|
1484
|
-
unit_amount = excluded.unit_amount,
|
|
1485
|
-
currency = excluded.currency,
|
|
1486
|
-
updated_at = excluded.updated_at
|
|
1487
|
-
`).run(tenantId, planVersionId, interval, targetStripePriceId, pricingType, setActive ? 1 : 0, unitAmount, currency, now, now);
|
|
1488
|
-
const mapping = db.prepare(`
|
|
1489
|
-
SELECT plan_version_id, billing_interval, stripe_price_id, pricing_type, active, unit_amount, currency, created_at, updated_at
|
|
1490
|
-
FROM billing_price_mappings
|
|
1491
|
-
WHERE tenant_id = ? AND plan_version_id = ? AND billing_interval = ? AND stripe_price_id = ?
|
|
1492
|
-
LIMIT 1
|
|
1493
|
-
`).get(tenantId, planVersionId, interval, targetStripePriceId);
|
|
1494
|
-
const afterRows = db.prepare(`
|
|
1495
|
-
SELECT plan_version_id, billing_interval, stripe_price_id, pricing_type, active, unit_amount, currency
|
|
1496
|
-
FROM billing_price_mappings
|
|
1497
|
-
WHERE tenant_id = ? AND plan_version_id = ? AND billing_interval = ?
|
|
1498
|
-
ORDER BY stripe_price_id ASC
|
|
1499
|
-
`).all(tenantId, planVersionId, interval);
|
|
1500
|
-
auditAdminAction(req, 'billing-price-mapping-upsert', {
|
|
1501
|
-
planVersionId,
|
|
1502
|
-
interval,
|
|
1503
|
-
pricingType,
|
|
1504
|
-
stripePriceId: pricingType === 'free' ? null : stripePriceId,
|
|
1505
|
-
active: setActive,
|
|
1506
|
-
before: beforeRows,
|
|
1507
|
-
after: afterRows
|
|
1508
|
-
});
|
|
1509
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1510
|
-
res.end(JSON.stringify({ mapping }));
|
|
1511
|
-
}
|
|
1512
|
-
catch (error) {
|
|
1513
|
-
const statusCode = resolveErrorStatusCode(error, 400);
|
|
1514
|
-
res.writeHead(statusCode, { 'Content-Type': 'application/json' });
|
|
1515
|
-
res.end(JSON.stringify({ error: String(error?.message || 'Unable to upsert price mapping'), code: error?.code }));
|
|
1516
|
-
}
|
|
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
|
+
}));
|
|
1517
1352
|
});
|
|
1518
1353
|
addRoute('POST', '/api/taskforce/admin/billing/price-mappings/deactivate', async (req, res) => {
|
|
1519
1354
|
if (!ensureCloudBilling(req, res))
|
|
@@ -1523,51 +1358,11 @@ export function registerBillingRoutes(deps) {
|
|
|
1523
1358
|
res.end(JSON.stringify({ error: 'Forbidden: system admin role required.' }));
|
|
1524
1359
|
return;
|
|
1525
1360
|
}
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
1532
|
-
res.end(JSON.stringify({ error: 'planVersionId and interval are required.', code: 'BILLING_MAPPING_TARGET_REQUIRED' }));
|
|
1533
|
-
return;
|
|
1534
|
-
}
|
|
1535
|
-
const db = core.getDatabaseAdapter();
|
|
1536
|
-
const tenantId = core.getTenantId();
|
|
1537
|
-
const now = new Date().toISOString();
|
|
1538
|
-
const beforeRows = db.prepare(`
|
|
1539
|
-
SELECT plan_version_id, billing_interval, stripe_price_id, pricing_type, active, unit_amount, currency
|
|
1540
|
-
FROM billing_price_mappings
|
|
1541
|
-
WHERE tenant_id = ? AND plan_version_id = ? AND billing_interval = ?
|
|
1542
|
-
ORDER BY stripe_price_id ASC
|
|
1543
|
-
`).all(tenantId, planVersionId, interval);
|
|
1544
|
-
const result = stripePriceId
|
|
1545
|
-
? db.prepare(`
|
|
1546
|
-
UPDATE billing_price_mappings
|
|
1547
|
-
SET active = 0, updated_at = ?
|
|
1548
|
-
WHERE tenant_id = ? AND plan_version_id = ? AND billing_interval = ? AND stripe_price_id = ?
|
|
1549
|
-
`).run(now, tenantId, planVersionId, interval, stripePriceId)
|
|
1550
|
-
: db.prepare(`
|
|
1551
|
-
UPDATE billing_price_mappings
|
|
1552
|
-
SET active = 0, updated_at = ?
|
|
1553
|
-
WHERE tenant_id = ? AND plan_version_id = ? AND billing_interval = ?
|
|
1554
|
-
`).run(now, tenantId, planVersionId, interval);
|
|
1555
|
-
const afterRows = db.prepare(`
|
|
1556
|
-
SELECT plan_version_id, billing_interval, stripe_price_id, pricing_type, active, unit_amount, currency
|
|
1557
|
-
FROM billing_price_mappings
|
|
1558
|
-
WHERE tenant_id = ? AND plan_version_id = ? AND billing_interval = ?
|
|
1559
|
-
ORDER BY stripe_price_id ASC
|
|
1560
|
-
`).all(tenantId, planVersionId, interval);
|
|
1561
|
-
auditAdminAction(req, 'billing-price-mapping-deactivate', {
|
|
1562
|
-
planVersionId,
|
|
1563
|
-
interval,
|
|
1564
|
-
stripePriceId: stripePriceId || null,
|
|
1565
|
-
changedRows: Number(result?.changes || 0),
|
|
1566
|
-
before: beforeRows,
|
|
1567
|
-
after: afterRows
|
|
1568
|
-
});
|
|
1569
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1570
|
-
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
|
+
}));
|
|
1571
1366
|
});
|
|
1572
1367
|
addRoute('GET', '/api/taskforce/admin/billing/webhook-failures', async (req, res) => {
|
|
1573
1368
|
if (!ensureCloudBilling(req, res))
|
|
@@ -1821,8 +1616,8 @@ export function registerBillingRoutes(deps) {
|
|
|
1821
1616
|
const stripe = getStripeClient();
|
|
1822
1617
|
let currentSubscriptionId = String(currentBilling?.stripe_subscription_id || '').trim();
|
|
1823
1618
|
const currentCustomerId = String(currentBilling?.stripe_customer_id || '').trim();
|
|
1824
|
-
|
|
1825
|
-
|
|
1619
|
+
let currentPlanVersionId = String(currentEntitlement?.plan_version_id || '').trim();
|
|
1620
|
+
let currentInterval = parseInterval(String(currentBilling?.billing_interval || '').trim());
|
|
1826
1621
|
const stripeAccountDiscovery = await discoverStripeAccountStateForUser(core, {
|
|
1827
1622
|
userId,
|
|
1828
1623
|
normalizedEmail: normalizedUserEmail,
|
|
@@ -1838,8 +1633,13 @@ export function registerBillingRoutes(deps) {
|
|
|
1838
1633
|
return;
|
|
1839
1634
|
}
|
|
1840
1635
|
let resolvedCustomerId = currentCustomerId || String(stripeAccountDiscovery.preferredCustomerId || '').trim();
|
|
1841
|
-
|
|
1842
|
-
|
|
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) {
|
|
1843
1643
|
currentSubscriptionId = String(activeSubscription.subscription?.id || '').trim();
|
|
1844
1644
|
resolvedCustomerId = String(activeSubscription.customerId || '').trim() || resolvedCustomerId;
|
|
1845
1645
|
if (currentSubscriptionId || resolvedCustomerId) {
|
|
@@ -1856,7 +1656,28 @@ export function registerBillingRoutes(deps) {
|
|
|
1856
1656
|
stripeCustomerId: resolvedCustomerId
|
|
1857
1657
|
});
|
|
1858
1658
|
}
|
|
1859
|
-
if (
|
|
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) {
|
|
1860
1681
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1861
1682
|
res.end(JSON.stringify({
|
|
1862
1683
|
checkoutState: 'already_current_plan',
|
|
@@ -1868,6 +1689,17 @@ export function registerBillingRoutes(deps) {
|
|
|
1868
1689
|
if (currentSubscriptionId) {
|
|
1869
1690
|
const current = await stripe.subscriptions.retrieve(currentSubscriptionId);
|
|
1870
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
|
+
}
|
|
1871
1703
|
if (!itemId) {
|
|
1872
1704
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
1873
1705
|
res.end(JSON.stringify({ error: 'Subscription has no updatable line item.' }));
|
|
@@ -1892,7 +1724,7 @@ export function registerBillingRoutes(deps) {
|
|
|
1892
1724
|
res.end(JSON.stringify({
|
|
1893
1725
|
checkoutState: 'updated',
|
|
1894
1726
|
planVersionId,
|
|
1895
|
-
message: 'Your subscription was
|
|
1727
|
+
message: 'Your subscription update was submitted. Changes will appear after Stripe confirms the update.'
|
|
1896
1728
|
}));
|
|
1897
1729
|
return;
|
|
1898
1730
|
}
|
|
@@ -2062,20 +1894,52 @@ export function registerBillingRoutes(deps) {
|
|
|
2062
1894
|
return;
|
|
2063
1895
|
}
|
|
2064
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
|
+
}
|
|
2065
1905
|
core.createUserBillingRecord(resolvedUserId);
|
|
2066
1906
|
upsertBillingMirror(core, {
|
|
2067
1907
|
userId: resolvedUserId,
|
|
2068
1908
|
stripeCustomerId: customerId || null,
|
|
2069
1909
|
stripeSubscriptionId: subscriptionId || null
|
|
2070
1910
|
});
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
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;
|
|
2078
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;
|
|
1941
|
+
}
|
|
1942
|
+
updateCheckoutAttemptStatus(core, sessionId, 'confirmed');
|
|
2079
1943
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
2080
1944
|
res.end(JSON.stringify({
|
|
2081
1945
|
success: true,
|
|
@@ -2140,7 +2004,7 @@ export function registerBillingRoutes(deps) {
|
|
|
2140
2004
|
const baseUrl = resolveBaseUrl(req);
|
|
2141
2005
|
const session = await stripe.billingPortal.sessions.create({
|
|
2142
2006
|
customer: customerId,
|
|
2143
|
-
return_url: `${baseUrl}
|
|
2007
|
+
return_url: `${baseUrl}/?screen=plans`
|
|
2144
2008
|
});
|
|
2145
2009
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
2146
2010
|
res.end(JSON.stringify({ url: session.url, returnBaseUrl: baseUrl }));
|
|
@@ -2385,12 +2249,15 @@ export function registerBillingRoutes(deps) {
|
|
|
2385
2249
|
});
|
|
2386
2250
|
return;
|
|
2387
2251
|
}
|
|
2388
|
-
|
|
2389
|
-
userId,
|
|
2252
|
+
core.transitionCanonicalCommercialState({
|
|
2253
|
+
accountId: userId,
|
|
2390
2254
|
planVersionId: planVersionId || existingEntitlement?.plan_version_id || core.getDefaultPlanVersionIdByMode('personal', { allowFallback: true }),
|
|
2391
2255
|
state: canonicalState,
|
|
2392
2256
|
effectiveAt: new Date().toISOString(),
|
|
2393
|
-
effectiveUntil: currentPeriodEnd
|
|
2257
|
+
effectiveUntil: currentPeriodEnd,
|
|
2258
|
+
billingMirrorPlanId: resolvePlanIdForVersion(core, planVersionId),
|
|
2259
|
+
source: 'stripe-webhook',
|
|
2260
|
+
updatedBy: 'stripe-webhook'
|
|
2394
2261
|
});
|
|
2395
2262
|
markLatestOpenCheckoutAttemptConfirmed(core, {
|
|
2396
2263
|
userId,
|