import type { BillingRefusal } from './api' import { type BillingCopy, DEFAULT_BILLING_COPY } from './copy' export interface BillingRefusalPresentation { action: { type: 'none' } | { type: 'portal'; url?: string } | { type: 'retry' } | { type: 'step_up' } message: string title: string } const portalAction = (url?: string): BillingRefusalPresentation['action'] => ({ type: 'portal', url }) const retryMessage = (refusal: BillingRefusal, copy: BillingCopy): string => { const mins = refusal.retryAfter ? copy.retryAfter(Math.max(1, Math.round(refusal.retryAfter / 60))) : '' return copy.rateLimited(mins) } const stripeRetryMessage = (refusal: BillingRefusal, copy: BillingCopy): string => { const mins = refusal.retryAfter ? copy.retryAfter(Math.max(1, Math.round(refusal.retryAfter / 60))) : '' return copy.stripeRetry(mins) } export const resolveRefusal = ( refusal: BillingRefusal, copy: BillingCopy = DEFAULT_BILLING_COPY ): BillingRefusalPresentation => { switch (refusal.kind) { case 'consent_required': return { action: portalAction(refusal.portalUrl), message: copy.confirmCardMessage, title: copy.confirmCardTitle } case 'insufficient_scope': return { action: { type: 'step_up' }, message: copy.spendingApprovalMessage, title: copy.spendingApprovalTitle } case 'remote_spending_revoked': { const who = refusal.actor === 'admin' ? copy.adminRevoked : copy.userRevoked return { action: portalAction(refusal.portalUrl), message: copy.reauthorize(who), title: copy.spendingRevoked } } case 'session_revoked': return { action: portalAction(refusal.portalUrl), message: copy.sessionRevokedMessage, title: copy.sessionRevokedTitle } case 'cli_billing_disabled': case 'remote_spending_disabled': return { action: portalAction(refusal.portalUrl), message: copy.spendingDisabledMessage, title: copy.spendingDisabledTitle } case 'role_required': return { action: portalAction(refusal.portalUrl), message: copy.adminRequiredMessage, title: copy.adminRequiredTitle } case 'idempotency_conflict': return { action: { type: 'none' }, message: copy.conflictingChargeMessage, title: copy.conflictingChargeTitle } case 'no_payment_method': return { action: portalAction(refusal.portalUrl), message: copy.noSavedCardMessage, title: copy.noSavedCard } case 'org_access_denied': return { action: { type: 'none' }, message: copy.orgDeniedMessage, title: copy.orgDeniedTitle } case 'monthly_cap_exceeded': { const remaining = refusal.payload?.remainingUsd return { action: portalAction(refusal.portalUrl), message: remaining != null ? copy.monthlyRemaining(String(remaining)) : copy.monthlyReached, title: copy.monthlyReachedTitle } } case 'rate_limited': case 'temporarily_unavailable': return { action: { type: 'retry' }, message: retryMessage(refusal, copy), title: copy.rateLimitedTitle } case 'stripe_unavailable': return { action: { type: 'retry' }, message: stripeRetryMessage(refusal, copy), title: copy.stripeTitle } case 'upgrade_cap_exceeded': return { action: { type: 'none' }, message: copy.dailyLimitMessage, title: copy.dailyLimitTitle } case 'endpoint_unavailable': return { action: { type: 'retry' }, message: refusal.message || copy.endpointMessage, title: copy.endpointTitle } case 'timeout': return { action: { type: 'retry' }, message: refusal.message || copy.requestTimeoutMessage, title: copy.requestTimeoutTitle } case 'transport': return { action: { type: 'retry' }, message: refusal.message || copy.connectionFailedMessage, title: copy.connectionFailedTitle } default: return { action: { type: 'none' }, message: refusal.message || copy.requestFailedMessage, title: copy.requestFailedTitle } } }