85 lines
2.6 KiB
TypeScript
85 lines
2.6 KiB
TypeScript
import type { KnownBillingRefusalCode } from '@hermes/shared/billing'
|
|
import { describe, expect, it } from 'vitest'
|
|
|
|
import type { BillingRefusal } from './api'
|
|
import { resolveRefusal } from './errors'
|
|
|
|
const expectedActions: Record<
|
|
KnownBillingRefusalCode | 'timeout' | 'transport',
|
|
'none' | 'portal' | 'retry' | 'step_up'
|
|
> = {
|
|
auto_top_up_disabled_failures: 'none',
|
|
cli_billing_disabled: 'portal',
|
|
consent_required: 'portal',
|
|
endpoint_unavailable: 'retry',
|
|
idempotency_conflict: 'none',
|
|
idempotency_key_required: 'none',
|
|
insufficient_scope: 'step_up',
|
|
internal_error: 'none',
|
|
invalid_charge_id: 'none',
|
|
invalid_request: 'none',
|
|
monthly_cap_exceeded: 'portal',
|
|
network_error: 'none',
|
|
no_payment_method: 'portal',
|
|
org_access_denied: 'none',
|
|
preview_rejected: 'none',
|
|
rate_limited: 'retry',
|
|
remote_spending_disabled: 'portal',
|
|
remote_spending_revoked: 'portal',
|
|
role_required: 'portal',
|
|
session_revoked: 'portal',
|
|
stripe_unavailable: 'retry',
|
|
temporarily_unavailable: 'retry',
|
|
timeout: 'retry',
|
|
transport: 'retry',
|
|
upgrade_cap_exceeded: 'none',
|
|
validation_failed: 'none'
|
|
}
|
|
|
|
describe('resolveRefusal', () => {
|
|
it('maps every known refusal kind to copy and the expected action', () => {
|
|
for (const [kind, actionType] of Object.entries(expectedActions)) {
|
|
const resolved = resolveRefusal({
|
|
kind: kind as BillingRefusal['kind'],
|
|
message: 'Server message.',
|
|
portalUrl: 'https://portal.nousresearch.com/billing',
|
|
retryAfter: 90
|
|
})
|
|
|
|
expect(resolved.title, kind).not.toHaveLength(0)
|
|
expect(resolved.message, kind).not.toHaveLength(0)
|
|
expect(resolved.action.type, kind).toBe(actionType)
|
|
}
|
|
})
|
|
|
|
it('includes monthly cap headroom when the server sends it', () => {
|
|
const resolved = resolveRefusal({
|
|
kind: 'monthly_cap_exceeded',
|
|
message: 'Monthly spend cap reached.',
|
|
payload: { remainingUsd: '4.50' }
|
|
})
|
|
|
|
expect(resolved.message).toContain('$4.50 headroom left')
|
|
})
|
|
|
|
it('includes Stripe retry timing when the server sends it', () => {
|
|
const resolved = resolveRefusal({
|
|
kind: 'stripe_unavailable',
|
|
message: 'Stripe is unavailable.',
|
|
retryAfter: 120
|
|
})
|
|
|
|
expect(resolved.message).toContain('try again in ~2 min')
|
|
})
|
|
|
|
it('falls back sanely for unknown refusal kinds', () => {
|
|
const resolved = resolveRefusal({ kind: 'new_billing_code', message: 'Something changed upstream.' })
|
|
|
|
expect(resolved).toEqual({
|
|
action: { type: 'none' },
|
|
message: 'Something changed upstream.',
|
|
title: 'Billing request failed'
|
|
})
|
|
})
|
|
})
|