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:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user