@taskforcehq/taskforce 0.3.302 → 0.3.303
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Taskforce.module.css +11 -0
- package/dist/TaskforceCore.js +59 -9
- package/dist/TaskforceCore.test.js +717 -14
- package/dist/components/task/TaskKanban.test.js +17 -0
- package/dist/components/views/PlansPage.d.ts +3 -1
- package/dist/components/views/PlansPage.js +110 -51
- package/dist/components/views/PlansPage.test.d.ts +1 -0
- package/dist/components/views/PlansPage.test.js +100 -0
- package/dist/components/views/StandaloneLayout.d.ts +1 -0
- package/dist/components/views/StandaloneLayout.js +106 -151
- 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/core/EntitlementsPolicy.test.js +2 -2
- package/dist/core/GlobalSettingsService.js +78 -18
- package/dist/core/PlanEntitlementService.d.ts +29 -5
- package/dist/core/PlanEntitlementService.js +297 -219
- package/dist/core/SignupMonetizationSettings.test.js +173 -49
- package/dist/core/SystemAdmin.test.js +186 -96
- package/dist/core/Taskforce.d.ts +51 -10
- package/dist/core/Taskforce.js +218 -49
- package/dist/core/Taskforce.mcpAuth.test.js +0 -2
- package/dist/core/UserProfileAvatarDrafts.test.js +12 -3
- package/dist/core/types.d.ts +11 -12
- package/dist/hooks/useSyncOrchestrator.js +17 -3
- package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +97 -5
- package/dist/hooks/useTaskforce.d.ts +3 -0
- package/dist/hooks/useTaskforce.js +9 -4
- package/dist/mcp/runtime.js +90 -29
- package/dist/mcp/runtime.test.js +272 -81
- package/dist/migrations/billingSchemaParity.test.js +3 -0
- package/dist/migrations/taskSchemaMigrations.js +15 -0
- package/dist/server/index.cookieProxy.test.js +1 -0
- package/dist/server/index.js +2 -0
- package/dist/server/index.test.js +2 -1
- package/dist/server/mockStripe.d.ts +1 -0
- package/dist/server/mockStripe.js +2 -0
- package/dist/server/routes/admin.js +80 -23
- package/dist/server/routes/auth.d.ts +1 -0
- package/dist/server/routes/auth.js +194 -53
- package/dist/server/routes/billing.js +506 -23
- package/dist/server/routes/billing.test.js +937 -45
- package/dist/server/routes/shared.js +5 -3
- package/dist/server/routes/sync.integration.test.js +160 -0
- package/dist/server/routes/sync.js +15 -5
- package/dist/server/routes.js +3 -3
- package/dist/server/routes.test.js +287 -76
- package/dist/sync/cloudSyncApi.d.ts +1 -0
- package/dist/sync/contentSyncState.d.ts +10 -0
- package/dist/sync/contentSyncState.js +277 -16
- package/dist/sync/syncService.d.ts +1 -0
- package/dist/sync/syncService.js +21 -8
- package/dist/sync/workspacePullFeed.d.ts +10 -0
- package/dist/sync/workspacePullFeed.js +111 -63
- package/dist/sync/workspacePullFeed.test.js +112 -0
- package/dist/ui/assets/{AgentsModule-JG5jc3he.js → AgentsModule-CpsTmrIW.js} +1 -1
- package/dist/ui/assets/{AnnotatedAttachmentWorkspace-Dvi02Y7q.js → AnnotatedAttachmentWorkspace-C_TmXZwA.js} +1 -1
- package/dist/ui/assets/{DocumentWorkspace-iys3zfca.js → DocumentWorkspace-C3GyzrWm.js} +1 -1
- package/dist/ui/assets/{InitiativesModule-DJvLJI3A.js → InitiativesModule-4-Rh2K2n.js} +1 -1
- package/dist/ui/assets/PlansPage-BP7AYOqr.js +1 -0
- package/dist/ui/assets/{TaskSettings-Bqd4cCzW.js → TaskSettings-BOC5F6Ag.js} +1 -1
- package/dist/ui/assets/{WorkflowsModule-BguHeQaV.js → WorkflowsModule-CGk9s3NJ.js} +1 -1
- package/dist/ui/assets/index-CLIMgR6g.js +6 -0
- package/dist/ui/assets/index-DneETxcu.css +1 -0
- package/dist/ui/assets/{vendor-icons-C8EED3Oh.js → vendor-icons-pWocEipT.js} +1 -1
- package/dist/ui/index.html +3 -3
- 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/package.json +3 -3
- package/dist/ui/assets/PlansPage-BMPlpFpw.js +0 -1
- 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,14 +486,304 @@ 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
|
}
|
|
496
|
+
function updateCheckoutAttemptStatus(core, sessionIdRaw, status) {
|
|
497
|
+
const sessionId = String(sessionIdRaw || '').trim();
|
|
498
|
+
if (!sessionId)
|
|
499
|
+
return;
|
|
500
|
+
const db = core.getDatabaseAdapter();
|
|
501
|
+
const tenantId = core.getTenantId();
|
|
502
|
+
db.prepare(`
|
|
503
|
+
UPDATE billing_checkout_attempts
|
|
504
|
+
SET status = ?, updated_at = ?
|
|
505
|
+
WHERE tenant_id = ? AND session_id = ?
|
|
506
|
+
`).run(status, new Date().toISOString(), tenantId, sessionId);
|
|
507
|
+
}
|
|
508
|
+
function expireOpenCheckoutAttempts(core) {
|
|
509
|
+
const db = core.getDatabaseAdapter();
|
|
510
|
+
const tenantId = core.getTenantId();
|
|
511
|
+
const now = new Date().toISOString();
|
|
512
|
+
const result = db.prepare(`
|
|
513
|
+
UPDATE billing_checkout_attempts
|
|
514
|
+
SET status = 'expired', updated_at = ?
|
|
515
|
+
WHERE tenant_id = ?
|
|
516
|
+
AND status = 'open'
|
|
517
|
+
AND expires_at IS NOT NULL
|
|
518
|
+
AND expires_at <> ''
|
|
519
|
+
AND expires_at <= ?
|
|
520
|
+
`).run(now, tenantId, now);
|
|
521
|
+
return Number(result?.changes || 0);
|
|
522
|
+
}
|
|
523
|
+
function purgeTerminalCheckoutAttempts(core) {
|
|
524
|
+
const db = core.getDatabaseAdapter();
|
|
525
|
+
const tenantId = core.getTenantId();
|
|
526
|
+
const cutoff = new Date(Date.now() - BILLING_CHECKOUT_ATTEMPT_RETENTION_DAYS * 24 * 60 * 60 * 1000).toISOString();
|
|
527
|
+
const result = db.prepare(`
|
|
528
|
+
DELETE FROM billing_checkout_attempts
|
|
529
|
+
WHERE tenant_id = ?
|
|
530
|
+
AND status IN ('confirmed', 'superseded', 'expired')
|
|
531
|
+
AND updated_at <= ?
|
|
532
|
+
`).run(tenantId, cutoff);
|
|
533
|
+
return Number(result?.changes || 0);
|
|
534
|
+
}
|
|
535
|
+
function maintainCheckoutAttempts(core) {
|
|
536
|
+
const expiredRows = expireOpenCheckoutAttempts(core);
|
|
537
|
+
const purgedRows = purgeTerminalCheckoutAttempts(core);
|
|
538
|
+
return { expiredRows, purgedRows };
|
|
539
|
+
}
|
|
540
|
+
function normalizeCheckoutAttemptRow(core, row) {
|
|
541
|
+
if (!row)
|
|
542
|
+
return null;
|
|
543
|
+
const sessionId = String(row.session_id || '').trim();
|
|
544
|
+
const status = String(row.status || '').trim().toLowerCase();
|
|
545
|
+
if (!sessionId || status !== 'open' || !isIsoTimestampExpired(row.expires_at)) {
|
|
546
|
+
return row;
|
|
547
|
+
}
|
|
548
|
+
updateCheckoutAttemptStatus(core, sessionId, 'expired');
|
|
549
|
+
return {
|
|
550
|
+
...row,
|
|
551
|
+
status: 'expired',
|
|
552
|
+
updated_at: new Date().toISOString()
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
function getCheckoutAttemptRow(core, sessionIdRaw) {
|
|
556
|
+
const sessionId = String(sessionIdRaw || '').trim();
|
|
557
|
+
if (!sessionId)
|
|
558
|
+
return null;
|
|
559
|
+
const db = core.getDatabaseAdapter();
|
|
560
|
+
const tenantId = core.getTenantId();
|
|
561
|
+
const row = db.prepare(`
|
|
562
|
+
SELECT session_id, user_id, plan_version_id, billing_interval, status, expires_at, created_at, updated_at
|
|
563
|
+
FROM billing_checkout_attempts
|
|
564
|
+
WHERE tenant_id = ? AND session_id = ?
|
|
565
|
+
LIMIT 1
|
|
566
|
+
`).get(tenantId, sessionId);
|
|
567
|
+
return normalizeCheckoutAttemptRow(core, row || null);
|
|
568
|
+
}
|
|
569
|
+
function getCurrentOpenCheckoutAttemptForUser(core, userIdRaw) {
|
|
570
|
+
const userId = String(userIdRaw || '').trim();
|
|
571
|
+
if (!userId)
|
|
572
|
+
return null;
|
|
573
|
+
const db = core.getDatabaseAdapter();
|
|
574
|
+
const tenantId = core.getTenantId();
|
|
575
|
+
const rows = db.prepare(`
|
|
576
|
+
SELECT session_id, user_id, plan_version_id, billing_interval, status, expires_at, created_at, updated_at
|
|
577
|
+
FROM billing_checkout_attempts
|
|
578
|
+
WHERE tenant_id = ? AND user_id = ? AND status = 'open'
|
|
579
|
+
ORDER BY created_at DESC, session_id DESC
|
|
580
|
+
`).all(tenantId, userId);
|
|
581
|
+
for (const row of rows) {
|
|
582
|
+
const normalized = normalizeCheckoutAttemptRow(core, row);
|
|
583
|
+
if (String(normalized?.status || '').trim().toLowerCase() === 'open') {
|
|
584
|
+
return normalized;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
return null;
|
|
588
|
+
}
|
|
589
|
+
function getLatestCheckoutAttemptForUser(core, userIdRaw) {
|
|
590
|
+
const userId = String(userIdRaw || '').trim();
|
|
591
|
+
if (!userId)
|
|
592
|
+
return null;
|
|
593
|
+
const db = core.getDatabaseAdapter();
|
|
594
|
+
const tenantId = core.getTenantId();
|
|
595
|
+
const row = db.prepare(`
|
|
596
|
+
SELECT session_id, user_id, plan_version_id, billing_interval, status, expires_at, created_at, updated_at
|
|
597
|
+
FROM billing_checkout_attempts
|
|
598
|
+
WHERE tenant_id = ? AND user_id = ?
|
|
599
|
+
ORDER BY created_at DESC, session_id DESC
|
|
600
|
+
LIMIT 1
|
|
601
|
+
`).get(tenantId, userId);
|
|
602
|
+
return normalizeCheckoutAttemptRow(core, row || null);
|
|
603
|
+
}
|
|
604
|
+
function resolveBillingConflictCode(core, input) {
|
|
605
|
+
const gate = String(input.gate || '').trim().toLowerCase();
|
|
606
|
+
if (!gate || gate === 'ok')
|
|
607
|
+
return null;
|
|
608
|
+
const latestAttempt = getLatestCheckoutAttemptForUser(core, input.userId);
|
|
609
|
+
const attemptStatus = String(latestAttempt?.status || '').trim().toLowerCase();
|
|
610
|
+
if (attemptStatus === 'expired')
|
|
611
|
+
return 'CHECKOUT_SESSION_EXPIRED';
|
|
612
|
+
if (attemptStatus === 'superseded')
|
|
613
|
+
return 'CHECKOUT_SESSION_SUPERSEDED';
|
|
614
|
+
return null;
|
|
615
|
+
}
|
|
616
|
+
function markLatestOpenCheckoutAttemptConfirmed(core, input) {
|
|
617
|
+
const planVersionId = String(input.planVersionId || '').trim();
|
|
618
|
+
if (!planVersionId)
|
|
619
|
+
return;
|
|
620
|
+
const currentAttempt = getCurrentOpenCheckoutAttemptForUser(core, input.userId);
|
|
621
|
+
if (!currentAttempt)
|
|
622
|
+
return;
|
|
623
|
+
const currentPlanVersionId = String(currentAttempt.plan_version_id || '').trim();
|
|
624
|
+
const sessionId = String(currentAttempt.session_id || '').trim();
|
|
625
|
+
if (!sessionId || currentPlanVersionId !== planVersionId)
|
|
626
|
+
return;
|
|
627
|
+
updateCheckoutAttemptStatus(core, sessionId, 'confirmed');
|
|
628
|
+
}
|
|
629
|
+
function recordCheckoutAttempt(core, input) {
|
|
630
|
+
const sessionId = String(input.sessionId || '').trim();
|
|
631
|
+
const userId = String(input.userId || '').trim();
|
|
632
|
+
const planVersionId = String(input.planVersionId || '').trim();
|
|
633
|
+
const billingInterval = parseInterval(input.billingInterval);
|
|
634
|
+
if (!sessionId || !userId || !planVersionId || !billingInterval)
|
|
635
|
+
return;
|
|
636
|
+
const db = core.getDatabaseAdapter();
|
|
637
|
+
const tenantId = core.getTenantId();
|
|
638
|
+
const now = new Date().toISOString();
|
|
639
|
+
const tx = db.transaction(() => {
|
|
640
|
+
db.prepare(`
|
|
641
|
+
UPDATE billing_checkout_attempts
|
|
642
|
+
SET status = 'superseded', updated_at = ?
|
|
643
|
+
WHERE tenant_id = ? AND user_id = ? AND status = 'open' AND session_id <> ?
|
|
644
|
+
`).run(now, tenantId, userId, sessionId);
|
|
645
|
+
db.prepare(`
|
|
646
|
+
INSERT INTO billing_checkout_attempts (
|
|
647
|
+
session_id, tenant_id, user_id, plan_version_id, billing_interval, status, expires_at, created_at, updated_at
|
|
648
|
+
) VALUES (?, ?, ?, ?, ?, 'open', ?, ?, ?)
|
|
649
|
+
ON CONFLICT (session_id) DO UPDATE SET
|
|
650
|
+
user_id = excluded.user_id,
|
|
651
|
+
plan_version_id = excluded.plan_version_id,
|
|
652
|
+
billing_interval = excluded.billing_interval,
|
|
653
|
+
status = 'open',
|
|
654
|
+
expires_at = excluded.expires_at,
|
|
655
|
+
updated_at = excluded.updated_at
|
|
656
|
+
`).run(sessionId, tenantId, userId, planVersionId, billingInterval, input.expiresAt || null, now, now);
|
|
657
|
+
});
|
|
658
|
+
tx();
|
|
659
|
+
}
|
|
660
|
+
function resolveCheckoutLifecycleGuard(core, input) {
|
|
661
|
+
const currentOpenAttempt = getCurrentOpenCheckoutAttemptForUser(core, input.userId);
|
|
662
|
+
const checkoutAttempt = input.sessionId
|
|
663
|
+
? getCheckoutAttemptRow(core, input.sessionId)
|
|
664
|
+
: currentOpenAttempt;
|
|
665
|
+
const attemptStatus = String(checkoutAttempt?.status || '').trim().toLowerCase();
|
|
666
|
+
const attemptPlanVersionId = String(checkoutAttempt?.plan_version_id || '').trim();
|
|
667
|
+
if (attemptStatus === 'expired') {
|
|
668
|
+
return {
|
|
669
|
+
blocked: true,
|
|
670
|
+
code: 'CHECKOUT_SESSION_EXPIRED',
|
|
671
|
+
message: 'This checkout session expired. Choose a plan and start checkout again.'
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
if (attemptStatus === 'superseded') {
|
|
675
|
+
return {
|
|
676
|
+
blocked: true,
|
|
677
|
+
code: 'CHECKOUT_SESSION_SUPERSEDED',
|
|
678
|
+
message: 'This checkout session is no longer current for this account.'
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
if (attemptStatus === 'open' && attemptPlanVersionId && input.planVersionId && attemptPlanVersionId !== input.planVersionId) {
|
|
682
|
+
return {
|
|
683
|
+
blocked: true,
|
|
684
|
+
code: 'CHECKOUT_SESSION_SUPERSEDED',
|
|
685
|
+
message: 'This checkout session is no longer current for this account.'
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
const currentBilling = getBillingMirrorRow(core, input.userId);
|
|
689
|
+
const currentSubscriptionId = String(currentBilling?.stripe_subscription_id || '').trim();
|
|
690
|
+
if (!currentOpenAttempt && input.subscriptionId && currentSubscriptionId && currentSubscriptionId !== input.subscriptionId) {
|
|
691
|
+
return {
|
|
692
|
+
blocked: true,
|
|
693
|
+
code: 'CHECKOUT_SESSION_SUPERSEDED',
|
|
694
|
+
message: 'This checkout session is no longer current for this account.'
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
const entitlement = getEntitlementRow(core, input.userId);
|
|
698
|
+
if (!entitlement)
|
|
699
|
+
return { blocked: false };
|
|
700
|
+
const currentState = String(entitlement.entitlement_state || '').trim().toLowerCase();
|
|
701
|
+
const currentPlanVersionId = String(entitlement.plan_version_id || '').trim();
|
|
702
|
+
const updatedBy = String(entitlement.updated_by || '').trim().toLowerCase();
|
|
703
|
+
const expiredPendingCheckout = currentState === 'checkout_pending' && isIsoTimestampExpired(entitlement.effective_until);
|
|
704
|
+
if (expiredPendingCheckout || (currentState === 'pending_plan_selection' && updatedBy === 'checkout-expired')) {
|
|
705
|
+
return {
|
|
706
|
+
blocked: true,
|
|
707
|
+
code: 'CHECKOUT_SESSION_EXPIRED',
|
|
708
|
+
message: 'This checkout session expired. Choose a plan and start checkout again.'
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
if (currentState === 'checkout_pending'
|
|
712
|
+
&& currentPlanVersionId
|
|
713
|
+
&& input.planVersionId
|
|
714
|
+
&& currentPlanVersionId !== input.planVersionId) {
|
|
715
|
+
return {
|
|
716
|
+
blocked: true,
|
|
717
|
+
code: 'CHECKOUT_SESSION_SUPERSEDED',
|
|
718
|
+
message: 'This checkout session is no longer current for this account.'
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
return { blocked: false };
|
|
722
|
+
}
|
|
723
|
+
function isCommercialStateAppAccessible(stateRaw) {
|
|
724
|
+
const state = String(stateRaw || '').trim().toLowerCase();
|
|
725
|
+
return state === 'active' || state === 'trialing' || state === 'grace';
|
|
726
|
+
}
|
|
727
|
+
function shouldRecordCheckoutPendingState(entitlement) {
|
|
728
|
+
if (!entitlement)
|
|
729
|
+
return true;
|
|
730
|
+
const currentState = String(entitlement.entitlement_state || '').trim().toLowerCase();
|
|
731
|
+
return currentState === 'pending_plan_selection'
|
|
732
|
+
|| currentState === 'checkout_pending'
|
|
733
|
+
|| currentState === 'canceled';
|
|
734
|
+
}
|
|
735
|
+
function beginCheckoutPending(core, input) {
|
|
736
|
+
const planId = resolvePlanIdForVersion(core, input.planVersionId);
|
|
737
|
+
const updatedBy = input.updatedBy || 'checkout-start';
|
|
738
|
+
const source = input.source || 'checkout-start';
|
|
739
|
+
if (typeof core.transitionCanonicalCommercialState === 'function') {
|
|
740
|
+
core.transitionCanonicalCommercialState({
|
|
741
|
+
accountId: input.userId,
|
|
742
|
+
planVersionId: input.planVersionId,
|
|
743
|
+
state: 'checkout_pending',
|
|
744
|
+
effectiveUntil: input.effectiveUntil || null,
|
|
745
|
+
billingMirrorPlanId: planId,
|
|
746
|
+
source,
|
|
747
|
+
updatedBy
|
|
748
|
+
});
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
const db = core.getDatabaseAdapter();
|
|
752
|
+
const tenantId = core.getTenantId();
|
|
753
|
+
const now = new Date().toISOString();
|
|
754
|
+
db.prepare(`
|
|
755
|
+
INSERT INTO account_entitlements (account_id, tenant_id, plan_version_id, entitlement_state, effective_at, effective_until, updated_by, created_at, updated_at)
|
|
756
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
757
|
+
ON CONFLICT (account_id) DO UPDATE SET
|
|
758
|
+
plan_version_id = excluded.plan_version_id,
|
|
759
|
+
entitlement_state = excluded.entitlement_state,
|
|
760
|
+
effective_at = excluded.effective_at,
|
|
761
|
+
effective_until = excluded.effective_until,
|
|
762
|
+
updated_by = excluded.updated_by,
|
|
763
|
+
updated_at = excluded.updated_at
|
|
764
|
+
`).run(input.userId, tenantId, input.planVersionId, 'checkout_pending', now, input.effectiveUntil || null, updatedBy, now, now);
|
|
765
|
+
upsertBillingMirror(core, {
|
|
766
|
+
userId: input.userId,
|
|
767
|
+
planId,
|
|
768
|
+
canonicalState: 'checkout_pending',
|
|
769
|
+
effectiveUntil: input.effectiveUntil || null
|
|
770
|
+
});
|
|
771
|
+
}
|
|
484
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
|
+
}
|
|
485
787
|
const db = core.getDatabaseAdapter();
|
|
486
788
|
const tenantId = core.getTenantId();
|
|
487
789
|
const now = new Date().toISOString();
|
|
@@ -495,7 +797,7 @@ function upsertCanonicalEntitlement(core, input) {
|
|
|
495
797
|
effective_until = excluded.effective_until,
|
|
496
798
|
updated_by = excluded.updated_by,
|
|
497
799
|
updated_at = excluded.updated_at
|
|
498
|
-
`).run(input.userId, tenantId, input.planVersionId, input.state, input.effectiveAt || now, input.effectiveUntil || null,
|
|
800
|
+
`).run(input.userId, tenantId, input.planVersionId, input.state, input.effectiveAt || now, input.effectiveUntil || null, updatedBy, now, now);
|
|
499
801
|
}
|
|
500
802
|
function upsertBillingMirror(core, input) {
|
|
501
803
|
const db = core.getDatabaseAdapter();
|
|
@@ -546,7 +848,12 @@ function applySubscriptionSnapshot(core, input) {
|
|
|
546
848
|
const canceledState = stripeStatus === 'canceled'
|
|
547
849
|
? (currentPeriodEnd && currentPeriodEnd > nowIso ? 'active' : 'canceled')
|
|
548
850
|
: null;
|
|
549
|
-
const
|
|
851
|
+
const mappedCanonicalState = canceledState || mapStripeStatusToCanonicalState(stripeStatus);
|
|
852
|
+
const preserveExistingAccess = mappedCanonicalState === 'checkout_pending'
|
|
853
|
+
&& isCommercialStateAppAccessible(existingEntitlement?.entitlement_state);
|
|
854
|
+
const canonicalState = preserveExistingAccess
|
|
855
|
+
? String(existingEntitlement?.entitlement_state || 'active').trim().toLowerCase()
|
|
856
|
+
: mappedCanonicalState;
|
|
550
857
|
const effectiveUntil = stripeStatus === 'canceled'
|
|
551
858
|
? currentPeriodEnd
|
|
552
859
|
: (currentPeriodEnd || null);
|
|
@@ -563,7 +870,17 @@ function applySubscriptionSnapshot(core, input) {
|
|
|
563
870
|
currentPeriodEnd,
|
|
564
871
|
effectiveUntil
|
|
565
872
|
});
|
|
566
|
-
if (planVersionId) {
|
|
873
|
+
if (planVersionId && canonicalState === 'checkout_pending') {
|
|
874
|
+
if (shouldRecordCheckoutPendingState(existingEntitlement)) {
|
|
875
|
+
beginCheckoutPending(core, {
|
|
876
|
+
userId,
|
|
877
|
+
planVersionId,
|
|
878
|
+
source,
|
|
879
|
+
updatedBy: source
|
|
880
|
+
});
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
else if (planVersionId) {
|
|
567
884
|
upsertCanonicalEntitlement(core, {
|
|
568
885
|
userId,
|
|
569
886
|
planVersionId: planVersionId || existingEntitlement?.plan_version_id || core.getDefaultPlanVersionIdByMode('personal', { allowFallback: true }),
|
|
@@ -934,12 +1251,23 @@ export function registerBillingRoutes(deps) {
|
|
|
934
1251
|
return;
|
|
935
1252
|
}
|
|
936
1253
|
try {
|
|
1254
|
+
maintainCheckoutAttempts(core);
|
|
937
1255
|
const userId = requestUserId(req);
|
|
938
1256
|
const db = core.getDatabaseAdapter();
|
|
939
1257
|
const tenantId = core.getTenantId();
|
|
940
|
-
const
|
|
941
|
-
const row = billingReconciliation.row;
|
|
1258
|
+
const row = getBillingMirrorRow(core, userId);
|
|
942
1259
|
const access = core.getAccountAccessStatus(userId);
|
|
1260
|
+
const billingConflictCode = resolveBillingConflictCode(core, {
|
|
1261
|
+
userId,
|
|
1262
|
+
gate: String(access.gate || '')
|
|
1263
|
+
});
|
|
1264
|
+
const resolvedPlanId = String(access.entitlement?.planId || row?.tier || '').trim() || null;
|
|
1265
|
+
const planRow = resolvedPlanId ? db.prepare(`
|
|
1266
|
+
SELECT display_name
|
|
1267
|
+
FROM plan_catalog
|
|
1268
|
+
WHERE tenant_id = ? AND plan_id = ?
|
|
1269
|
+
LIMIT 1
|
|
1270
|
+
`).get(tenantId, resolvedPlanId) : undefined;
|
|
943
1271
|
if (!access.hasEntitlement || !access.entitlement) {
|
|
944
1272
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
945
1273
|
res.end(JSON.stringify({
|
|
@@ -951,24 +1279,18 @@ export function registerBillingRoutes(deps) {
|
|
|
951
1279
|
trialEnd: row?.trial_end || null,
|
|
952
1280
|
currentPeriodEnd: row?.current_period_end || null,
|
|
953
1281
|
effectiveUntil: row?.effective_until || null,
|
|
954
|
-
entitlementState: null,
|
|
1282
|
+
entitlementState: row?.entitlement_state || null,
|
|
955
1283
|
planVersionId: null,
|
|
956
|
-
planId:
|
|
957
|
-
planName:
|
|
1284
|
+
planId: resolvedPlanId,
|
|
1285
|
+
planName: planRow?.display_name ? String(planRow.display_name) : resolvedPlanId,
|
|
958
1286
|
gate: access.gate,
|
|
959
1287
|
message: access.message,
|
|
960
1288
|
planSelectionRequired: access.planSelectionRequired,
|
|
961
|
-
billingConflictCode
|
|
1289
|
+
billingConflictCode
|
|
962
1290
|
}));
|
|
963
1291
|
return;
|
|
964
1292
|
}
|
|
965
1293
|
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
1294
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
973
1295
|
res.end(JSON.stringify({
|
|
974
1296
|
stripeStatus: row?.stripe_status || null,
|
|
@@ -986,7 +1308,7 @@ export function registerBillingRoutes(deps) {
|
|
|
986
1308
|
gate: access.gate,
|
|
987
1309
|
message: access.message,
|
|
988
1310
|
planSelectionRequired: access.planSelectionRequired,
|
|
989
|
-
billingConflictCode
|
|
1311
|
+
billingConflictCode
|
|
990
1312
|
}));
|
|
991
1313
|
}
|
|
992
1314
|
catch (error) {
|
|
@@ -1290,6 +1612,68 @@ export function registerBillingRoutes(deps) {
|
|
|
1290
1612
|
rows
|
|
1291
1613
|
}));
|
|
1292
1614
|
});
|
|
1615
|
+
addRoute('GET', '/api/taskforce/admin/billing/checkout-attempts', async (req, res) => {
|
|
1616
|
+
if (!ensureCloudBilling(req, res))
|
|
1617
|
+
return;
|
|
1618
|
+
if (!ensureAdminRole(req)) {
|
|
1619
|
+
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
1620
|
+
res.end(JSON.stringify({ error: 'Forbidden: system admin role required.' }));
|
|
1621
|
+
return;
|
|
1622
|
+
}
|
|
1623
|
+
const db = core.getDatabaseAdapter();
|
|
1624
|
+
const tenantId = core.getTenantId();
|
|
1625
|
+
const requestUrl = parseRequestUrl(req);
|
|
1626
|
+
const limit = parsePositiveIntBounded(requestUrl.searchParams.get('limit'), 50, 1, 200);
|
|
1627
|
+
const filterUserId = String(requestUrl.searchParams.get('userId') || '').trim();
|
|
1628
|
+
const now = new Date().toISOString();
|
|
1629
|
+
const maintenance = maintainCheckoutAttempts(core);
|
|
1630
|
+
const rows = filterUserId
|
|
1631
|
+
? db.prepare(`
|
|
1632
|
+
SELECT session_id, user_id, plan_version_id, billing_interval, status, expires_at, created_at, updated_at
|
|
1633
|
+
FROM billing_checkout_attempts
|
|
1634
|
+
WHERE tenant_id = ? AND user_id = ?
|
|
1635
|
+
ORDER BY created_at DESC, session_id DESC
|
|
1636
|
+
LIMIT ?
|
|
1637
|
+
`).all(tenantId, filterUserId, limit)
|
|
1638
|
+
: db.prepare(`
|
|
1639
|
+
SELECT session_id, user_id, plan_version_id, billing_interval, status, expires_at, created_at, updated_at
|
|
1640
|
+
FROM billing_checkout_attempts
|
|
1641
|
+
WHERE tenant_id = ?
|
|
1642
|
+
ORDER BY created_at DESC, session_id DESC
|
|
1643
|
+
LIMIT ?
|
|
1644
|
+
`).all(tenantId, limit);
|
|
1645
|
+
const total = filterUserId
|
|
1646
|
+
? db.prepare(`
|
|
1647
|
+
SELECT COUNT(*) AS count
|
|
1648
|
+
FROM billing_checkout_attempts
|
|
1649
|
+
WHERE tenant_id = ? AND user_id = ?
|
|
1650
|
+
`).get(tenantId, filterUserId)
|
|
1651
|
+
: db.prepare(`
|
|
1652
|
+
SELECT COUNT(*) AS count
|
|
1653
|
+
FROM billing_checkout_attempts
|
|
1654
|
+
WHERE tenant_id = ?
|
|
1655
|
+
`).get(tenantId);
|
|
1656
|
+
const openCount = filterUserId
|
|
1657
|
+
? db.prepare(`
|
|
1658
|
+
SELECT COUNT(*) AS count
|
|
1659
|
+
FROM billing_checkout_attempts
|
|
1660
|
+
WHERE tenant_id = ? AND user_id = ? AND status = 'open'
|
|
1661
|
+
`).get(tenantId, filterUserId)
|
|
1662
|
+
: db.prepare(`
|
|
1663
|
+
SELECT COUNT(*) AS count
|
|
1664
|
+
FROM billing_checkout_attempts
|
|
1665
|
+
WHERE tenant_id = ? AND status = 'open'
|
|
1666
|
+
`).get(tenantId);
|
|
1667
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1668
|
+
res.end(JSON.stringify({
|
|
1669
|
+
asOf: now,
|
|
1670
|
+
expiredRowsNormalized: maintenance.expiredRows,
|
|
1671
|
+
purgedTerminalRows: maintenance.purgedRows,
|
|
1672
|
+
total: Number(total?.count || 0),
|
|
1673
|
+
openCount: Number(openCount?.count || 0),
|
|
1674
|
+
rows
|
|
1675
|
+
}));
|
|
1676
|
+
});
|
|
1293
1677
|
addRoute('POST', '/api/taskforce/admin/billing/prices/import', async (req, res) => {
|
|
1294
1678
|
if (!ensureCloudBilling(req, res))
|
|
1295
1679
|
return;
|
|
@@ -1388,6 +1772,7 @@ export function registerBillingRoutes(deps) {
|
|
|
1388
1772
|
return;
|
|
1389
1773
|
}
|
|
1390
1774
|
try {
|
|
1775
|
+
maintainCheckoutAttempts(core);
|
|
1391
1776
|
const userId = requestUserId(req);
|
|
1392
1777
|
const currentBillingResult = await reconcileBillingMirrorIfNeeded(core, userId);
|
|
1393
1778
|
const currentBilling = currentBillingResult.row;
|
|
@@ -1551,6 +1936,21 @@ export function registerBillingRoutes(deps) {
|
|
|
1551
1936
|
billingInterval: interval
|
|
1552
1937
|
}
|
|
1553
1938
|
});
|
|
1939
|
+
const checkoutEffectiveUntil = isoFromUnixSeconds(session.expires_at);
|
|
1940
|
+
recordCheckoutAttempt(core, {
|
|
1941
|
+
sessionId: session.id,
|
|
1942
|
+
userId,
|
|
1943
|
+
planVersionId,
|
|
1944
|
+
billingInterval: interval,
|
|
1945
|
+
expiresAt: checkoutEffectiveUntil
|
|
1946
|
+
});
|
|
1947
|
+
if (shouldRecordCheckoutPendingState(currentEntitlement) && !isCommercialStateAppAccessible(currentEntitlement?.entitlement_state)) {
|
|
1948
|
+
beginCheckoutPending(core, {
|
|
1949
|
+
userId,
|
|
1950
|
+
planVersionId,
|
|
1951
|
+
effectiveUntil: checkoutEffectiveUntil
|
|
1952
|
+
});
|
|
1953
|
+
}
|
|
1554
1954
|
const checkoutUrl = isMockStripeClient(stripe)
|
|
1555
1955
|
? buildMockStripeHostedCheckoutUrl(resolveHostedServerBaseUrl(req), session.id)
|
|
1556
1956
|
: (session.url || null);
|
|
@@ -1592,11 +1992,30 @@ export function registerBillingRoutes(deps) {
|
|
|
1592
1992
|
return;
|
|
1593
1993
|
}
|
|
1594
1994
|
try {
|
|
1995
|
+
maintainCheckoutAttempts(core);
|
|
1595
1996
|
const stripe = getStripeClient();
|
|
1596
1997
|
const expectedUserId = requestUserId(req);
|
|
1597
1998
|
const session = await stripe.checkout.sessions.retrieve(sessionId, {
|
|
1598
1999
|
expand: ['subscription']
|
|
1599
2000
|
});
|
|
2001
|
+
const sessionStatus = String(session.status || '').trim().toLowerCase();
|
|
2002
|
+
const paymentStatus = String(session.payment_status || '').trim().toLowerCase();
|
|
2003
|
+
if (session.mode !== 'subscription') {
|
|
2004
|
+
res.writeHead(409, { 'Content-Type': 'application/json' });
|
|
2005
|
+
res.end(JSON.stringify({
|
|
2006
|
+
error: 'Checkout session is not a subscription checkout.',
|
|
2007
|
+
code: 'CHECKOUT_SESSION_INVALID_MODE'
|
|
2008
|
+
}));
|
|
2009
|
+
return;
|
|
2010
|
+
}
|
|
2011
|
+
if (sessionStatus !== 'complete' || (paymentStatus !== 'paid' && paymentStatus !== 'no_payment_required')) {
|
|
2012
|
+
res.writeHead(409, { 'Content-Type': 'application/json' });
|
|
2013
|
+
res.end(JSON.stringify({
|
|
2014
|
+
error: 'Checkout session has not completed yet.',
|
|
2015
|
+
code: 'CHECKOUT_SESSION_INCOMPLETE'
|
|
2016
|
+
}));
|
|
2017
|
+
return;
|
|
2018
|
+
}
|
|
1600
2019
|
const metadata = session.metadata || {};
|
|
1601
2020
|
const customerId = String(session.customer || '').trim();
|
|
1602
2021
|
const subscriptionId = typeof session.subscription === 'string'
|
|
@@ -1615,22 +2034,47 @@ export function registerBillingRoutes(deps) {
|
|
|
1615
2034
|
res.end(JSON.stringify({ error: 'Checkout session does not belong to the current account.' }));
|
|
1616
2035
|
return;
|
|
1617
2036
|
}
|
|
2037
|
+
let resolvedPlanVersionId = null;
|
|
2038
|
+
let subscription = null;
|
|
2039
|
+
if (subscriptionId) {
|
|
2040
|
+
subscription = typeof session.subscription === 'string' || !session.subscription
|
|
2041
|
+
? await stripe.subscriptions.retrieve(subscriptionId)
|
|
2042
|
+
: session.subscription;
|
|
2043
|
+
const priceId = String(subscription.items?.data?.[0]?.price?.id || '').trim();
|
|
2044
|
+
resolvedPlanVersionId = resolvePlanVersionIdForBillingEvent(core, [
|
|
2045
|
+
subscription.metadata?.planVersionId,
|
|
2046
|
+
subscription.metadata?.plan_version_id,
|
|
2047
|
+
metadata.planVersionId,
|
|
2048
|
+
metadata.plan_version_id
|
|
2049
|
+
], priceId).planVersionId;
|
|
2050
|
+
const lifecycleGuard = resolveCheckoutLifecycleGuard(core, {
|
|
2051
|
+
userId: resolvedUserId,
|
|
2052
|
+
planVersionId: resolvedPlanVersionId,
|
|
2053
|
+
sessionId,
|
|
2054
|
+
subscriptionId
|
|
2055
|
+
});
|
|
2056
|
+
if (lifecycleGuard.blocked) {
|
|
2057
|
+
res.writeHead(409, { 'Content-Type': 'application/json' });
|
|
2058
|
+
res.end(JSON.stringify({
|
|
2059
|
+
error: lifecycleGuard.message,
|
|
2060
|
+
code: lifecycleGuard.code
|
|
2061
|
+
}));
|
|
2062
|
+
return;
|
|
2063
|
+
}
|
|
2064
|
+
}
|
|
1618
2065
|
core.createUserBillingRecord(resolvedUserId);
|
|
1619
2066
|
upsertBillingMirror(core, {
|
|
1620
2067
|
userId: resolvedUserId,
|
|
1621
2068
|
stripeCustomerId: customerId || null,
|
|
1622
2069
|
stripeSubscriptionId: subscriptionId || null
|
|
1623
2070
|
});
|
|
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;
|
|
2071
|
+
if (subscription) {
|
|
1629
2072
|
resolvedPlanVersionId = applySubscriptionSnapshot(core, {
|
|
1630
2073
|
userId: resolvedUserId,
|
|
1631
2074
|
subscription,
|
|
1632
2075
|
source: 'checkout-confirm'
|
|
1633
2076
|
}).planVersionId;
|
|
2077
|
+
updateCheckoutAttemptStatus(core, sessionId, 'confirmed');
|
|
1634
2078
|
}
|
|
1635
2079
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1636
2080
|
res.end(JSON.stringify({
|
|
@@ -1801,6 +2245,7 @@ export function registerBillingRoutes(deps) {
|
|
|
1801
2245
|
const db = core.getDatabaseAdapter();
|
|
1802
2246
|
const tenantId = core.getTenantId();
|
|
1803
2247
|
purgeExpiredWebhookFailures(core);
|
|
2248
|
+
maintainCheckoutAttempts(core);
|
|
1804
2249
|
let duplicate = false;
|
|
1805
2250
|
const tx = db.transaction(() => {
|
|
1806
2251
|
const acquired = lockWebhookEvent(core, event);
|
|
@@ -1853,11 +2298,31 @@ export function registerBillingRoutes(deps) {
|
|
|
1853
2298
|
});
|
|
1854
2299
|
return;
|
|
1855
2300
|
}
|
|
1856
|
-
|
|
2301
|
+
const lifecycleGuard = resolveCheckoutLifecycleGuard(core, {
|
|
2302
|
+
userId,
|
|
2303
|
+
planVersionId,
|
|
2304
|
+
subscriptionId: String(subscription.id || '').trim()
|
|
2305
|
+
});
|
|
2306
|
+
if (lifecycleGuard.blocked) {
|
|
2307
|
+
recordWebhookFailure(core, {
|
|
2308
|
+
eventId: event.id,
|
|
2309
|
+
eventType: event.type,
|
|
2310
|
+
reason: lifecycleGuard.message,
|
|
2311
|
+
payload: event
|
|
2312
|
+
});
|
|
2313
|
+
return;
|
|
2314
|
+
}
|
|
2315
|
+
const snapshot = applySubscriptionSnapshot(core, {
|
|
1857
2316
|
userId,
|
|
1858
2317
|
subscription,
|
|
1859
2318
|
source: 'stripe-webhook'
|
|
1860
2319
|
});
|
|
2320
|
+
if (snapshot.canonicalState !== 'checkout_pending') {
|
|
2321
|
+
markLatestOpenCheckoutAttemptConfirmed(core, {
|
|
2322
|
+
userId,
|
|
2323
|
+
planVersionId: snapshot.planVersionId
|
|
2324
|
+
});
|
|
2325
|
+
}
|
|
1861
2326
|
return;
|
|
1862
2327
|
}
|
|
1863
2328
|
if (event.type === 'invoice.paid' || event.type === 'invoice.payment_failed') {
|
|
@@ -1878,6 +2343,20 @@ export function registerBillingRoutes(deps) {
|
|
|
1878
2343
|
invoice.subscription_details?.metadata?.plan_version_id
|
|
1879
2344
|
], priceId);
|
|
1880
2345
|
const planVersionId = planVersionResolution.planVersionId;
|
|
2346
|
+
const lifecycleGuard = resolveCheckoutLifecycleGuard(core, {
|
|
2347
|
+
userId,
|
|
2348
|
+
planVersionId,
|
|
2349
|
+
subscriptionId
|
|
2350
|
+
});
|
|
2351
|
+
if (lifecycleGuard.blocked) {
|
|
2352
|
+
recordWebhookFailure(core, {
|
|
2353
|
+
eventId: event.id,
|
|
2354
|
+
eventType: event.type,
|
|
2355
|
+
reason: lifecycleGuard.message,
|
|
2356
|
+
payload: event
|
|
2357
|
+
});
|
|
2358
|
+
return;
|
|
2359
|
+
}
|
|
1881
2360
|
const existingEntitlement = getEntitlementRow(core, userId);
|
|
1882
2361
|
const canonicalState = event.type === 'invoice.payment_failed' ? 'grace' : 'active';
|
|
1883
2362
|
const currentPeriodEnd = invoice.lines?.data?.[0]?.period?.end
|
|
@@ -1913,6 +2392,10 @@ export function registerBillingRoutes(deps) {
|
|
|
1913
2392
|
effectiveAt: new Date().toISOString(),
|
|
1914
2393
|
effectiveUntil: currentPeriodEnd
|
|
1915
2394
|
});
|
|
2395
|
+
markLatestOpenCheckoutAttemptConfirmed(core, {
|
|
2396
|
+
userId,
|
|
2397
|
+
planVersionId
|
|
2398
|
+
});
|
|
1916
2399
|
return;
|
|
1917
2400
|
}
|
|
1918
2401
|
});
|