Public Access
1
0

feat: launch Stripe billing flows with lifecycle hardening and analytics

add Stripe checkout, portal, webhook ingestion, and idempotent event persistence

add billing lifecycle state (grace/sync/timeline/admin visibility) and stronger entitlement handling

add analytics event tracking and admin summary APIs plus account/pricing UI integration
This commit is contained in:
pguerrerox
2026-05-22 22:55:04 +00:00
parent 94b8c357b4
commit 5508e15da1
35 changed files with 2851 additions and 50 deletions
+46
View File
@@ -1,4 +1,5 @@
import type { Pool, PoolClient } from 'pg';
import { resolveBillingAccessState } from '../../../shared/billing/lifecycle.js';
import type { AccountBillingState } from '../../../shared/types.js';
import type { EntitlementDecision, UsageAction, UsageAmount, UsageCostEstimate, UsageResource } from '../../../shared/billing/entitlements.js';
import { evaluateActionEntitlement, isActivePlanCodeForEntitlements } from '../../../shared/billing/entitlements.js';
@@ -73,6 +74,31 @@ export async function checkActionEntitlementForWorkspace(
};
}
const billingAccess = resolveBillingAccessState({
status: billing.status,
currentPeriodEndsAt: billing.currentPeriodEndsAt,
cancelAtPeriodEnd: billing.cancelAtPeriodEnd,
gracePeriodEndsAt: billing.gracePeriodEndsAt,
});
if (input.costEstimate.isChargeable && billingAccess.accessMode === 'blocked') {
return {
allowed: false,
decision: {
status: 'blocked_upgrade_required',
denialReason: mapBillingStatusToDenialReason(billing.status),
action: input.action,
resource: getPrimaryUsageAmount(input.costEstimate)?.resource ?? 'research_credits',
requiredAmount: getPrimaryUsageAmount(input.costEstimate)?.amount ?? 0,
remainingAmount: 0,
currentPlanCode: billing.planCode,
suggestedUpgradePlanCode: billing.planCode,
addonEligible: false,
contactSalesRequired: false,
},
};
}
if (!input.costEstimate.isChargeable) {
return {
allowed: true,
@@ -165,6 +191,12 @@ function formatEntitlementErrorMessage(decision: EntitlementDecision) {
switch (decision.denialReason) {
case 'billing_not_configured':
return 'A billing plan is required before this action can run.';
case 'billing_past_due':
return 'Payment is overdue and billing access is currently blocked.';
case 'billing_canceled':
return 'This subscription is canceled. Reactivate billing to continue.';
case 'billing_inactive':
return 'Billing is inactive for this workspace.';
case 'feature_not_available':
return 'Your current plan does not include this feature.';
case 'not_launch_ready':
@@ -178,5 +210,19 @@ function formatEntitlementErrorMessage(decision: EntitlementDecision) {
}
}
function mapBillingStatusToDenialReason(status: AccountBillingState['status']) {
switch (status) {
case 'past_due':
return 'billing_past_due' as const;
case 'canceled':
return 'billing_canceled' as const;
case 'inactive':
case 'not_configured':
return 'billing_inactive' as const;
case 'active':
return 'billing_inactive' as const;
}
}
// Export policy exists in shared billing modules, but route-level export enforcement
// stays deferred until export generation moves to a backend endpoint.