157 lines
4.3 KiB
TypeScript
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
|
|
}
|
|
}
|
|
}
|