@taskforcehq/taskforce 0.3.304 → 0.3.305
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +140 -247
- package/dist/TaskforceCore.js +165 -70
- package/dist/TaskforceCore.test.js +799 -90
- package/dist/compat/workspaceSyncCompat.js +6 -0
- 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 +29 -3
- package/dist/components/features/TaskSettings.test.js +42 -1
- 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/TaskForm.d.ts +8 -8
- package/dist/components/task/TaskForm.js +358 -221
- package/dist/components/task/TaskForm.test.js +456 -381
- package/dist/components/task/TaskKanban.js +2 -1
- package/dist/components/views/PlanComparisonPage.js +5 -8
- package/dist/components/views/PlanComparisonPage.test.js +51 -41
- package/dist/components/views/PlansPage.js +126 -84
- package/dist/components/views/PlansPage.test.js +308 -39
- package/dist/components/views/StandaloneLayout.js +28 -22
- 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/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/EntitlementsPolicy.test.js +75 -13
- package/dist/core/PlanEntitlementService.d.ts +7 -1
- package/dist/core/PlanEntitlementService.js +117 -42
- package/dist/core/PlanVersionPolicy.test.js +19 -26
- package/dist/core/SignupMonetizationSettings.test.js +46 -21
- package/dist/core/SystemAdmin.test.js +212 -56
- package/dist/core/TaskTaxonomyValidation.test.js +53 -0
- package/dist/core/Taskforce.d.ts +3 -0
- package/dist/core/Taskforce.js +50 -10
- package/dist/core/WorkspacePlanMode.test.js +12 -12
- package/dist/core/shared.d.ts +2 -0
- package/dist/core/shared.js +11 -0
- package/dist/hooks/sync/auth.js +3 -1
- package/dist/hooks/sync/transfers.d.ts +2 -1
- package/dist/hooks/sync/transfers.js +8 -4
- package/dist/hooks/useSyncOrchestrator.d.ts +7 -4
- package/dist/hooks/useSyncOrchestrator.js +75 -7
- package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +80 -10
- package/dist/hooks/useTaskMutations.d.ts +2 -1
- package/dist/hooks/useTaskMutations.js +12 -1
- package/dist/hooks/useTaskMutations.test.js +30 -1
- package/dist/hooks/useTaskforce.d.ts +5 -2
- package/dist/hooks/useTaskforce.js +17 -5
- package/dist/hooks/useTaskforce.sync-behavior.test.js +78 -0
- package/dist/hooks/useWorkspaceSyncController.d.ts +3 -1
- package/dist/hooks/useWorkspaceSyncController.js +11 -1
- package/dist/hooks/useWorkspaceSyncController.test.js +1 -0
- package/dist/mcp/adminWorkspaceRegistrar.d.ts +2 -0
- package/dist/mcp/adminWorkspaceRegistrar.js +154 -0
- 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/runtime.js +2362 -3530
- package/dist/mcp/runtime.test.js +159 -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/server/routes/admin.d.ts +1 -5
- package/dist/server/routes/admin.js +138 -61
- 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 +23 -63
- 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 +104 -188
- package/dist/server/routes/billing.test.js +214 -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 +1 -1
- package/dist/server/routes/sync.d.ts +1 -1
- package/dist/server/routes/sync.js +1 -1
- package/dist/server/routes/tasks.d.ts +1 -1
- package/dist/server/routes/tasks.js +1 -1
- 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 +76 -162
- package/dist/server/routes.test.js +672 -30
- package/dist/sync/contentSyncState.js +16 -7
- package/dist/sync/syncApplyHandlers.test.js +2 -0
- package/dist/sync/syncService.d.ts +1 -0
- package/dist/sync/syncService.js +8 -0
- package/dist/sync/syncService.test.js +14 -0
- package/dist/sync/workspaceSyncModel.d.ts +3 -0
- package/dist/sync/workspaceSyncModel.js +12 -3
- package/dist/sync/workspaceSyncModel.test.js +34 -0
- package/dist/sync/workspaceSyncState.d.ts +2 -0
- package/dist/sync/workspaceSyncState.js +1 -0
- package/dist/ui/assets/{AgentsModule-BLWVbuKP.js → AgentsModule-N2MnQBZk.js} +1 -1
- package/dist/ui/assets/{AnnotatedAttachmentWorkspace-BlS-cqnl.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-BjR6iBf9.js → InitiativesModule-Dhm7M8e7.js} +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-DnFqdUME.js → WorkflowsModule-BcS4afRn.js} +1 -1
- package/dist/ui/assets/{index-Ii0B0rTO.css → index-C2-OXZV7.css} +1 -1
- package/dist/ui/assets/index-DcSI5F86.js +6 -0
- package/dist/ui/assets/{vendor-icons-I7ZwG1z_.js → vendor-icons-BSUaRwF8.js} +1 -1
- package/dist/ui/index.html +3 -3
- package/dist/utils/commercialLifecyclePresentation.d.ts +19 -0
- package/dist/utils/commercialLifecyclePresentation.js +196 -0
- package/package.json +8 -1
- package/scripts/check-cloud-mcp.mjs +83 -0
- package/scripts/playwright-auth.sh +5 -1
- package/scripts/run-billing-performance-smoke.sh +24 -0
- package/dist/ui/assets/DocumentWorkspace-BH_6DF6x.js +0 -250
- package/dist/ui/assets/DocumentWorkspace-C_8T8oz-.css +0 -1
- package/dist/ui/assets/PlansPage-CSDQ4sLa.js +0 -1
- package/dist/ui/assets/TaskSettings-CEDy34Jo.js +0 -9
- package/dist/ui/assets/index-DXUdtH1B.js +0 -6
|
@@ -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';
|
|
@@ -795,11 +795,15 @@ function applySubscriptionSnapshot(core, input) {
|
|
|
795
795
|
? (currentPeriodEnd && currentPeriodEnd > nowIso ? 'active' : 'canceled')
|
|
796
796
|
: null;
|
|
797
797
|
const mappedCanonicalState = canceledState || mapStripeStatusToCanonicalState(stripeStatus);
|
|
798
|
+
const existingPlanVersionId = String(existingEntitlement?.plan_version_id || '').trim();
|
|
798
799
|
const preserveExistingAccess = mappedCanonicalState === 'checkout_pending'
|
|
800
|
+
&& Boolean(planVersionId)
|
|
801
|
+
&& existingPlanVersionId === planVersionId
|
|
799
802
|
&& isCommercialStateAppAccessible(existingEntitlement?.entitlement_state);
|
|
800
803
|
const canonicalState = preserveExistingAccess
|
|
801
804
|
? String(existingEntitlement?.entitlement_state || 'active').trim().toLowerCase()
|
|
802
805
|
: mappedCanonicalState;
|
|
806
|
+
const billingConfirmedSource = source === 'stripe-webhook' || source === 'checkout-confirm';
|
|
803
807
|
const effectiveUntil = stripeStatus === 'canceled'
|
|
804
808
|
? currentPeriodEnd
|
|
805
809
|
: (currentPeriodEnd || null);
|
|
@@ -826,7 +830,7 @@ function applySubscriptionSnapshot(core, input) {
|
|
|
826
830
|
});
|
|
827
831
|
}
|
|
828
832
|
}
|
|
829
|
-
else if (planVersionId) {
|
|
833
|
+
else if (planVersionId && billingConfirmedSource) {
|
|
830
834
|
core.transitionCanonicalCommercialState({
|
|
831
835
|
accountId: userId,
|
|
832
836
|
planVersionId: planVersionId || existingEntitlement?.plan_version_id || core.getDefaultPlanVersionIdByMode('personal', { allowFallback: true }),
|
|
@@ -1326,18 +1330,11 @@ export function registerBillingRoutes(deps) {
|
|
|
1326
1330
|
res.end(JSON.stringify({ error: 'Forbidden: system admin role required.' }));
|
|
1327
1331
|
return;
|
|
1328
1332
|
}
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
res.end(JSON.stringify({ options }));
|
|
1335
|
-
}
|
|
1336
|
-
catch (error) {
|
|
1337
|
-
const statusCode = resolveErrorStatusCode(error, 400);
|
|
1338
|
-
res.writeHead(statusCode, { 'Content-Type': 'application/json' });
|
|
1339
|
-
res.end(JSON.stringify({ error: String(error?.message || 'Unable to list Stripe prices'), code: error?.code }));
|
|
1340
|
-
}
|
|
1333
|
+
res.writeHead(410, { 'Content-Type': 'application/json' });
|
|
1334
|
+
res.end(JSON.stringify({
|
|
1335
|
+
error: 'Manual Stripe price selection has been removed. Refresh prices from Stripe metadata instead.',
|
|
1336
|
+
code: 'BILLING_PRICE_MANUAL_MAPPING_REMOVED'
|
|
1337
|
+
}));
|
|
1341
1338
|
});
|
|
1342
1339
|
addRoute('PUT', '/api/taskforce/admin/billing/price-mappings', async (req, res) => {
|
|
1343
1340
|
if (!ensureCloudBilling(req, res))
|
|
@@ -1347,121 +1344,11 @@ export function registerBillingRoutes(deps) {
|
|
|
1347
1344
|
res.end(JSON.stringify({ error: 'Forbidden: system admin role required.' }));
|
|
1348
1345
|
return;
|
|
1349
1346
|
}
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
const setActive = body?.active === false ? false : true;
|
|
1356
|
-
if (!planVersionId || !interval) {
|
|
1357
|
-
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
1358
|
-
res.end(JSON.stringify({ error: 'planVersionId and interval are required.', code: 'BILLING_MAPPING_INPUT_REQUIRED' }));
|
|
1359
|
-
return;
|
|
1360
|
-
}
|
|
1361
|
-
if (pricingType === 'stripe' && !stripePriceId) {
|
|
1362
|
-
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
1363
|
-
res.end(JSON.stringify({ error: 'stripePriceId is required for Stripe pricing.', code: 'BILLING_MAPPING_STRIPE_PRICE_REQUIRED' }));
|
|
1364
|
-
return;
|
|
1365
|
-
}
|
|
1366
|
-
const db = core.getDatabaseAdapter();
|
|
1367
|
-
const tenantId = core.getTenantId();
|
|
1368
|
-
const versionExists = db.prepare(`
|
|
1369
|
-
SELECT plan_version_id
|
|
1370
|
-
FROM plan_versions
|
|
1371
|
-
WHERE tenant_id = ? AND plan_version_id = ?
|
|
1372
|
-
LIMIT 1
|
|
1373
|
-
`).get(tenantId, planVersionId);
|
|
1374
|
-
if (!versionExists?.plan_version_id) {
|
|
1375
|
-
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
1376
|
-
res.end(JSON.stringify({ error: 'Plan version not found.', code: 'PLAN_VERSION_NOT_FOUND' }));
|
|
1377
|
-
return;
|
|
1378
|
-
}
|
|
1379
|
-
try {
|
|
1380
|
-
const now = new Date().toISOString();
|
|
1381
|
-
const targetStripePriceId = pricingType === 'free'
|
|
1382
|
-
? buildFreePricingSyntheticId(planVersionId, interval)
|
|
1383
|
-
: stripePriceId;
|
|
1384
|
-
let unitAmount = 0;
|
|
1385
|
-
let currency = null;
|
|
1386
|
-
if (pricingType === 'stripe') {
|
|
1387
|
-
const stripe = getStripeClient();
|
|
1388
|
-
const stripePrice = await stripe.prices.retrieve(stripePriceId);
|
|
1389
|
-
const stripeInterval = parseInterval(stripePrice.recurring?.interval || '');
|
|
1390
|
-
if (!stripePrice.active) {
|
|
1391
|
-
res.writeHead(422, { 'Content-Type': 'application/json' });
|
|
1392
|
-
res.end(JSON.stringify({ error: 'Stripe price is not active.', code: 'STRIPE_PRICE_INACTIVE' }));
|
|
1393
|
-
return;
|
|
1394
|
-
}
|
|
1395
|
-
if (!stripeInterval) {
|
|
1396
|
-
res.writeHead(422, { 'Content-Type': 'application/json' });
|
|
1397
|
-
res.end(JSON.stringify({ error: 'Stripe price must be recurring month/year.', code: 'STRIPE_PRICE_INTERVAL_UNSUPPORTED' }));
|
|
1398
|
-
return;
|
|
1399
|
-
}
|
|
1400
|
-
if (stripeInterval !== interval) {
|
|
1401
|
-
res.writeHead(422, { 'Content-Type': 'application/json' });
|
|
1402
|
-
res.end(JSON.stringify({ error: `Stripe price interval (${stripeInterval}) does not match requested interval (${interval}).`, code: 'STRIPE_PRICE_INTERVAL_MISMATCH' }));
|
|
1403
|
-
return;
|
|
1404
|
-
}
|
|
1405
|
-
unitAmount = typeof stripePrice.unit_amount === 'number' ? stripePrice.unit_amount : null;
|
|
1406
|
-
currency = stripePrice.currency || null;
|
|
1407
|
-
}
|
|
1408
|
-
const beforeRows = db.prepare(`
|
|
1409
|
-
SELECT plan_version_id, billing_interval, stripe_price_id, pricing_type, active, unit_amount, currency
|
|
1410
|
-
FROM billing_price_mappings
|
|
1411
|
-
WHERE tenant_id = ? AND plan_version_id = ? AND billing_interval = ?
|
|
1412
|
-
ORDER BY stripe_price_id ASC
|
|
1413
|
-
`).all(tenantId, planVersionId, interval);
|
|
1414
|
-
if (setActive) {
|
|
1415
|
-
db.prepare(`
|
|
1416
|
-
UPDATE billing_price_mappings
|
|
1417
|
-
SET active = 0, updated_at = ?
|
|
1418
|
-
WHERE tenant_id = ?
|
|
1419
|
-
AND plan_version_id = ?
|
|
1420
|
-
AND billing_interval = ?
|
|
1421
|
-
AND stripe_price_id <> ?
|
|
1422
|
-
AND active = 1
|
|
1423
|
-
`).run(now, tenantId, planVersionId, interval, targetStripePriceId);
|
|
1424
|
-
}
|
|
1425
|
-
db.prepare(`
|
|
1426
|
-
INSERT INTO billing_price_mappings (
|
|
1427
|
-
tenant_id, plan_version_id, billing_interval, stripe_price_id, pricing_type, active, unit_amount, currency, created_at, updated_at
|
|
1428
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1429
|
-
ON CONFLICT (tenant_id, plan_version_id, billing_interval, stripe_price_id) DO UPDATE SET
|
|
1430
|
-
pricing_type = excluded.pricing_type,
|
|
1431
|
-
active = excluded.active,
|
|
1432
|
-
unit_amount = excluded.unit_amount,
|
|
1433
|
-
currency = excluded.currency,
|
|
1434
|
-
updated_at = excluded.updated_at
|
|
1435
|
-
`).run(tenantId, planVersionId, interval, targetStripePriceId, pricingType, setActive ? 1 : 0, unitAmount, currency, now, now);
|
|
1436
|
-
const mapping = db.prepare(`
|
|
1437
|
-
SELECT plan_version_id, billing_interval, stripe_price_id, pricing_type, active, unit_amount, currency, created_at, updated_at
|
|
1438
|
-
FROM billing_price_mappings
|
|
1439
|
-
WHERE tenant_id = ? AND plan_version_id = ? AND billing_interval = ? AND stripe_price_id = ?
|
|
1440
|
-
LIMIT 1
|
|
1441
|
-
`).get(tenantId, planVersionId, interval, targetStripePriceId);
|
|
1442
|
-
const afterRows = db.prepare(`
|
|
1443
|
-
SELECT plan_version_id, billing_interval, stripe_price_id, pricing_type, active, unit_amount, currency
|
|
1444
|
-
FROM billing_price_mappings
|
|
1445
|
-
WHERE tenant_id = ? AND plan_version_id = ? AND billing_interval = ?
|
|
1446
|
-
ORDER BY stripe_price_id ASC
|
|
1447
|
-
`).all(tenantId, planVersionId, interval);
|
|
1448
|
-
auditAdminAction(req, 'billing-price-mapping-upsert', {
|
|
1449
|
-
planVersionId,
|
|
1450
|
-
interval,
|
|
1451
|
-
pricingType,
|
|
1452
|
-
stripePriceId: pricingType === 'free' ? null : stripePriceId,
|
|
1453
|
-
active: setActive,
|
|
1454
|
-
before: beforeRows,
|
|
1455
|
-
after: afterRows
|
|
1456
|
-
});
|
|
1457
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1458
|
-
res.end(JSON.stringify({ mapping }));
|
|
1459
|
-
}
|
|
1460
|
-
catch (error) {
|
|
1461
|
-
const statusCode = resolveErrorStatusCode(error, 400);
|
|
1462
|
-
res.writeHead(statusCode, { 'Content-Type': 'application/json' });
|
|
1463
|
-
res.end(JSON.stringify({ error: String(error?.message || 'Unable to upsert price mapping'), code: error?.code }));
|
|
1464
|
-
}
|
|
1347
|
+
res.writeHead(410, { 'Content-Type': 'application/json' });
|
|
1348
|
+
res.end(JSON.stringify({
|
|
1349
|
+
error: 'Manual billing price mappings have been removed. Import Stripe prices instead.',
|
|
1350
|
+
code: 'BILLING_PRICE_MANUAL_MAPPING_REMOVED'
|
|
1351
|
+
}));
|
|
1465
1352
|
});
|
|
1466
1353
|
addRoute('POST', '/api/taskforce/admin/billing/price-mappings/deactivate', async (req, res) => {
|
|
1467
1354
|
if (!ensureCloudBilling(req, res))
|
|
@@ -1471,51 +1358,11 @@ export function registerBillingRoutes(deps) {
|
|
|
1471
1358
|
res.end(JSON.stringify({ error: 'Forbidden: system admin role required.' }));
|
|
1472
1359
|
return;
|
|
1473
1360
|
}
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
1480
|
-
res.end(JSON.stringify({ error: 'planVersionId and interval are required.', code: 'BILLING_MAPPING_TARGET_REQUIRED' }));
|
|
1481
|
-
return;
|
|
1482
|
-
}
|
|
1483
|
-
const db = core.getDatabaseAdapter();
|
|
1484
|
-
const tenantId = core.getTenantId();
|
|
1485
|
-
const now = new Date().toISOString();
|
|
1486
|
-
const beforeRows = db.prepare(`
|
|
1487
|
-
SELECT plan_version_id, billing_interval, stripe_price_id, pricing_type, active, unit_amount, currency
|
|
1488
|
-
FROM billing_price_mappings
|
|
1489
|
-
WHERE tenant_id = ? AND plan_version_id = ? AND billing_interval = ?
|
|
1490
|
-
ORDER BY stripe_price_id ASC
|
|
1491
|
-
`).all(tenantId, planVersionId, interval);
|
|
1492
|
-
const result = stripePriceId
|
|
1493
|
-
? db.prepare(`
|
|
1494
|
-
UPDATE billing_price_mappings
|
|
1495
|
-
SET active = 0, updated_at = ?
|
|
1496
|
-
WHERE tenant_id = ? AND plan_version_id = ? AND billing_interval = ? AND stripe_price_id = ?
|
|
1497
|
-
`).run(now, tenantId, planVersionId, interval, stripePriceId)
|
|
1498
|
-
: db.prepare(`
|
|
1499
|
-
UPDATE billing_price_mappings
|
|
1500
|
-
SET active = 0, updated_at = ?
|
|
1501
|
-
WHERE tenant_id = ? AND plan_version_id = ? AND billing_interval = ?
|
|
1502
|
-
`).run(now, tenantId, planVersionId, interval);
|
|
1503
|
-
const afterRows = db.prepare(`
|
|
1504
|
-
SELECT plan_version_id, billing_interval, stripe_price_id, pricing_type, active, unit_amount, currency
|
|
1505
|
-
FROM billing_price_mappings
|
|
1506
|
-
WHERE tenant_id = ? AND plan_version_id = ? AND billing_interval = ?
|
|
1507
|
-
ORDER BY stripe_price_id ASC
|
|
1508
|
-
`).all(tenantId, planVersionId, interval);
|
|
1509
|
-
auditAdminAction(req, 'billing-price-mapping-deactivate', {
|
|
1510
|
-
planVersionId,
|
|
1511
|
-
interval,
|
|
1512
|
-
stripePriceId: stripePriceId || null,
|
|
1513
|
-
changedRows: Number(result?.changes || 0),
|
|
1514
|
-
before: beforeRows,
|
|
1515
|
-
after: afterRows
|
|
1516
|
-
});
|
|
1517
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1518
|
-
res.end(JSON.stringify({ success: true, changedRows: Number(result?.changes || 0) }));
|
|
1361
|
+
res.writeHead(410, { 'Content-Type': 'application/json' });
|
|
1362
|
+
res.end(JSON.stringify({
|
|
1363
|
+
error: 'Manual billing price mappings have been removed. Refresh Stripe prices instead.',
|
|
1364
|
+
code: 'BILLING_PRICE_MANUAL_MAPPING_REMOVED'
|
|
1365
|
+
}));
|
|
1519
1366
|
});
|
|
1520
1367
|
addRoute('GET', '/api/taskforce/admin/billing/webhook-failures', async (req, res) => {
|
|
1521
1368
|
if (!ensureCloudBilling(req, res))
|
|
@@ -1769,8 +1616,8 @@ export function registerBillingRoutes(deps) {
|
|
|
1769
1616
|
const stripe = getStripeClient();
|
|
1770
1617
|
let currentSubscriptionId = String(currentBilling?.stripe_subscription_id || '').trim();
|
|
1771
1618
|
const currentCustomerId = String(currentBilling?.stripe_customer_id || '').trim();
|
|
1772
|
-
|
|
1773
|
-
|
|
1619
|
+
let currentPlanVersionId = String(currentEntitlement?.plan_version_id || '').trim();
|
|
1620
|
+
let currentInterval = parseInterval(String(currentBilling?.billing_interval || '').trim());
|
|
1774
1621
|
const stripeAccountDiscovery = await discoverStripeAccountStateForUser(core, {
|
|
1775
1622
|
userId,
|
|
1776
1623
|
normalizedEmail: normalizedUserEmail,
|
|
@@ -1786,8 +1633,13 @@ export function registerBillingRoutes(deps) {
|
|
|
1786
1633
|
return;
|
|
1787
1634
|
}
|
|
1788
1635
|
let resolvedCustomerId = currentCustomerId || String(stripeAccountDiscovery.preferredCustomerId || '').trim();
|
|
1789
|
-
|
|
1790
|
-
|
|
1636
|
+
const activeSubscription = stripeAccountDiscovery.activeSubscriptions.length === 1
|
|
1637
|
+
? stripeAccountDiscovery.activeSubscriptions[0]
|
|
1638
|
+
: null;
|
|
1639
|
+
let discoveredPlanVersionId = null;
|
|
1640
|
+
let discoveredInterval = null;
|
|
1641
|
+
let discoveredStripePriceId = null;
|
|
1642
|
+
if (!currentSubscriptionId && activeSubscription) {
|
|
1791
1643
|
currentSubscriptionId = String(activeSubscription.subscription?.id || '').trim();
|
|
1792
1644
|
resolvedCustomerId = String(activeSubscription.customerId || '').trim() || resolvedCustomerId;
|
|
1793
1645
|
if (currentSubscriptionId || resolvedCustomerId) {
|
|
@@ -1804,7 +1656,28 @@ export function registerBillingRoutes(deps) {
|
|
|
1804
1656
|
stripeCustomerId: resolvedCustomerId
|
|
1805
1657
|
});
|
|
1806
1658
|
}
|
|
1807
|
-
if (
|
|
1659
|
+
if (activeSubscription) {
|
|
1660
|
+
const activeSubscriptionPriceId = String(activeSubscription.subscription?.items?.data?.[0]?.price?.id || '').trim();
|
|
1661
|
+
discoveredStripePriceId = activeSubscriptionPriceId || null;
|
|
1662
|
+
const activePlanVersionId = resolvePlanVersionIdForBillingEvent(core, [
|
|
1663
|
+
activeSubscription.subscription?.metadata?.planVersionId,
|
|
1664
|
+
activeSubscription.subscription?.metadata?.plan_version_id
|
|
1665
|
+
], activeSubscriptionPriceId).planVersionId;
|
|
1666
|
+
discoveredPlanVersionId = activePlanVersionId;
|
|
1667
|
+
discoveredInterval = parseInterval(activeSubscription.subscription?.items?.data?.[0]?.price?.recurring?.interval || '');
|
|
1668
|
+
if (!currentPlanVersionId && activePlanVersionId) {
|
|
1669
|
+
currentPlanVersionId = activePlanVersionId;
|
|
1670
|
+
}
|
|
1671
|
+
if (!currentInterval) {
|
|
1672
|
+
currentInterval = discoveredInterval;
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
const isCurrentRequestedPlan = (currentPlanVersionId === planVersionId && currentInterval === interval) || (Boolean(currentSubscriptionId)
|
|
1676
|
+
&& discoveredPlanVersionId === planVersionId
|
|
1677
|
+
&& discoveredInterval === interval) || (Boolean(currentSubscriptionId)
|
|
1678
|
+
&& discoveredStripePriceId === priceId
|
|
1679
|
+
&& discoveredInterval === interval);
|
|
1680
|
+
if (currentSubscriptionId && isCurrentRequestedPlan) {
|
|
1808
1681
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1809
1682
|
res.end(JSON.stringify({
|
|
1810
1683
|
checkoutState: 'already_current_plan',
|
|
@@ -1816,6 +1689,17 @@ export function registerBillingRoutes(deps) {
|
|
|
1816
1689
|
if (currentSubscriptionId) {
|
|
1817
1690
|
const current = await stripe.subscriptions.retrieve(currentSubscriptionId);
|
|
1818
1691
|
const itemId = current.items.data[0]?.id;
|
|
1692
|
+
const currentStripePriceId = String(current.items.data[0]?.price?.id || '').trim();
|
|
1693
|
+
const currentSubscriptionInterval = parseInterval(current.items.data[0]?.price?.recurring?.interval || '');
|
|
1694
|
+
if (currentStripePriceId === priceId && currentSubscriptionInterval === interval) {
|
|
1695
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1696
|
+
res.end(JSON.stringify({
|
|
1697
|
+
checkoutState: 'already_current_plan',
|
|
1698
|
+
planVersionId,
|
|
1699
|
+
message: 'You are already on this plan.'
|
|
1700
|
+
}));
|
|
1701
|
+
return;
|
|
1702
|
+
}
|
|
1819
1703
|
if (!itemId) {
|
|
1820
1704
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
1821
1705
|
res.end(JSON.stringify({ error: 'Subscription has no updatable line item.' }));
|
|
@@ -1840,7 +1724,7 @@ export function registerBillingRoutes(deps) {
|
|
|
1840
1724
|
res.end(JSON.stringify({
|
|
1841
1725
|
checkoutState: 'updated',
|
|
1842
1726
|
planVersionId,
|
|
1843
|
-
message: 'Your subscription was
|
|
1727
|
+
message: 'Your subscription update was submitted. Changes will appear after Stripe confirms the update.'
|
|
1844
1728
|
}));
|
|
1845
1729
|
return;
|
|
1846
1730
|
}
|
|
@@ -2010,20 +1894,52 @@ export function registerBillingRoutes(deps) {
|
|
|
2010
1894
|
return;
|
|
2011
1895
|
}
|
|
2012
1896
|
}
|
|
1897
|
+
if (!subscriptionId || !subscription) {
|
|
1898
|
+
res.writeHead(409, { 'Content-Type': 'application/json' });
|
|
1899
|
+
res.end(JSON.stringify({
|
|
1900
|
+
error: 'Checkout session is still finalizing your subscription.',
|
|
1901
|
+
code: 'CHECKOUT_SESSION_INCOMPLETE'
|
|
1902
|
+
}));
|
|
1903
|
+
return;
|
|
1904
|
+
}
|
|
2013
1905
|
core.createUserBillingRecord(resolvedUserId);
|
|
2014
1906
|
upsertBillingMirror(core, {
|
|
2015
1907
|
userId: resolvedUserId,
|
|
2016
1908
|
stripeCustomerId: customerId || null,
|
|
2017
1909
|
stripeSubscriptionId: subscriptionId || null
|
|
2018
1910
|
});
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
1911
|
+
const snapshot = applySubscriptionSnapshot(core, {
|
|
1912
|
+
userId: resolvedUserId,
|
|
1913
|
+
subscription,
|
|
1914
|
+
source: 'checkout-confirm'
|
|
1915
|
+
});
|
|
1916
|
+
resolvedPlanVersionId = snapshot.planVersionId;
|
|
1917
|
+
if (!resolvedPlanVersionId) {
|
|
1918
|
+
res.writeHead(409, { 'Content-Type': 'application/json' });
|
|
1919
|
+
res.end(JSON.stringify({
|
|
1920
|
+
error: 'Checkout completed, but the purchased plan could not be resolved yet.',
|
|
1921
|
+
code: 'CHECKOUT_PLAN_VERSION_UNRESOLVED'
|
|
1922
|
+
}));
|
|
1923
|
+
return;
|
|
1924
|
+
}
|
|
1925
|
+
const updatedEntitlement = getEntitlementRow(core, resolvedUserId);
|
|
1926
|
+
const updatedPlanVersionId = String(updatedEntitlement?.plan_version_id || '').trim();
|
|
1927
|
+
const updatedState = String(updatedEntitlement?.entitlement_state || '').trim().toLowerCase();
|
|
1928
|
+
const canonicalSettled = snapshot.canonicalState !== 'checkout_pending'
|
|
1929
|
+
&& updatedPlanVersionId === resolvedPlanVersionId
|
|
1930
|
+
&& updatedState === snapshot.canonicalState;
|
|
1931
|
+
if (!canonicalSettled) {
|
|
1932
|
+
console.warn(`[Taskforce Billing] Checkout confirmation still settling for user=${resolvedUserId} session=${sessionId} subscription=${subscriptionId || '(missing)'} `
|
|
1933
|
+
+ `planVersionId=${resolvedPlanVersionId} canonicalState=${snapshot.canonicalState} persistedPlanVersionId=${updatedPlanVersionId || '(missing)'} `
|
|
1934
|
+
+ `persistedState=${updatedState || '(missing)'}`);
|
|
1935
|
+
res.writeHead(409, { 'Content-Type': 'application/json' });
|
|
1936
|
+
res.end(JSON.stringify({
|
|
1937
|
+
error: 'Checkout session is still finalizing your plan.',
|
|
1938
|
+
code: 'CHECKOUT_SESSION_INCOMPLETE'
|
|
1939
|
+
}));
|
|
1940
|
+
return;
|
|
2026
1941
|
}
|
|
1942
|
+
updateCheckoutAttemptStatus(core, sessionId, 'confirmed');
|
|
2027
1943
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
2028
1944
|
res.end(JSON.stringify({
|
|
2029
1945
|
success: true,
|
|
@@ -2088,7 +2004,7 @@ export function registerBillingRoutes(deps) {
|
|
|
2088
2004
|
const baseUrl = resolveBaseUrl(req);
|
|
2089
2005
|
const session = await stripe.billingPortal.sessions.create({
|
|
2090
2006
|
customer: customerId,
|
|
2091
|
-
return_url: `${baseUrl}
|
|
2007
|
+
return_url: `${baseUrl}/?screen=plans`
|
|
2092
2008
|
});
|
|
2093
2009
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
2094
2010
|
res.end(JSON.stringify({ url: session.url, returnBaseUrl: baseUrl }));
|