Files
aiturk-hermes-ide/apps/desktop/src/app/settings/billing/errors.ts
T

157 lines
4.3 KiB
TypeScript

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
}
}
}