feat(contributions): rewrite server config as preset-based helpers
This commit is contained in:
parent
03eee45cbd
commit
55af652263
2 changed files with 98 additions and 39 deletions
|
|
@ -1,28 +1,29 @@
|
||||||
// Server-side contribution config
|
// Server-side contribution config.
|
||||||
export const CONTRIBUTION_TIERS = {
|
// Parallel to app/config/contributions.js but also hosts getHelcimPlanId,
|
||||||
FREE: { value: "0", amount: 0, label: "$0 - I need support right now", tier: "free" },
|
// which is server-only (depends on runtime config).
|
||||||
SUPPORTER: { value: "5", amount: 5, label: "$5 - I can contribute", tier: "supporter" },
|
|
||||||
MEMBER: { value: "15", amount: 15, label: "$15 - I can sustain the community", tier: "member" },
|
export const CONTRIBUTION_PRESETS = [
|
||||||
ADVOCATE: { value: "30", amount: 30, label: "$30 - I can support others too", tier: "advocate" },
|
{ amount: 0, label: "I need support right now" },
|
||||||
CHAMPION: { value: "50", amount: 50, label: "$50 - I want to sponsor multiple members", tier: "champion" },
|
{ amount: 5, label: "I can contribute" },
|
||||||
|
{ amount: 15, label: "I can sustain the community" },
|
||||||
|
{ amount: 30, label: "I can support others too" },
|
||||||
|
{ amount: 50, label: "I want to sponsor multiple members" },
|
||||||
|
]
|
||||||
|
|
||||||
|
export const requiresPayment = (amount) => amount > 0
|
||||||
|
|
||||||
|
export const isValidContributionAmount = (amount) =>
|
||||||
|
Number.isInteger(amount) && amount >= 0
|
||||||
|
|
||||||
|
export const getGuidanceLabel = (amount) => {
|
||||||
|
if (amount === null || amount === undefined) return null
|
||||||
|
const n = Number(amount)
|
||||||
|
if (!Number.isFinite(n) || n < 0) return null
|
||||||
|
const match = CONTRIBUTION_PRESETS.findLast(p => p.amount <= n)
|
||||||
|
return match?.label ?? null
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getContributionOptions = () => Object.values(CONTRIBUTION_TIERS)
|
// Cadence-keyed plan-id lookup. Unrelated to the tier concept.
|
||||||
export const getValidContributionValues = () => Object.values(CONTRIBUTION_TIERS).map(t => t.value)
|
|
||||||
export const getContributionTierByValue = (value) =>
|
|
||||||
Object.values(CONTRIBUTION_TIERS).find(t => t.value === value)
|
|
||||||
export const requiresPayment = (value) => (getContributionTierByValue(value)?.amount ?? 0) > 0
|
|
||||||
export const isValidContributionValue = (value) => getValidContributionValues().includes(value)
|
|
||||||
export const getPaidContributionTiers = () =>
|
|
||||||
Object.values(CONTRIBUTION_TIERS).filter(t => t.amount > 0)
|
|
||||||
|
|
||||||
export function getTierAmount(tier, cadence = 'monthly') {
|
|
||||||
const base = parseFloat(tier.amount)
|
|
||||||
return cadence === 'annual' ? base * 10 : base
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cadence-keyed plan-id lookup. Throws via createError at call site if env missing;
|
|
||||||
// this helper just returns the raw runtime-config value.
|
|
||||||
export function getHelcimPlanId(cadence) {
|
export function getHelcimPlanId(cadence) {
|
||||||
const config = useRuntimeConfig()
|
const config = useRuntimeConfig()
|
||||||
if (cadence === 'annual') return config.helcimAnnualPlanId || null
|
if (cadence === 'annual') return config.helcimAnnualPlanId || null
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,78 @@
|
||||||
import { describe, it, expect } from 'vitest'
|
import { describe, it, expect } from 'vitest'
|
||||||
import { getTierAmount } from '../../../server/config/contributions.js'
|
import {
|
||||||
|
CONTRIBUTION_PRESETS,
|
||||||
|
requiresPayment,
|
||||||
|
isValidContributionAmount,
|
||||||
|
getGuidanceLabel,
|
||||||
|
} from '../../../server/config/contributions.js'
|
||||||
|
|
||||||
describe('getTierAmount', () => {
|
describe('CONTRIBUTION_PRESETS', () => {
|
||||||
it('returns monthly amount as-is', () => {
|
it('exposes the five suggested amounts with guidance labels', () => {
|
||||||
expect(getTierAmount({ amount: 5 }, 'monthly')).toBe(5)
|
expect(CONTRIBUTION_PRESETS).toEqual([
|
||||||
})
|
{ amount: 0, label: 'I need support right now' },
|
||||||
|
{ amount: 5, label: 'I can contribute' },
|
||||||
it('returns annual amount as base * 10', () => {
|
{ amount: 15, label: 'I can sustain the community' },
|
||||||
expect(getTierAmount({ amount: 5 }, 'annual')).toBe(50)
|
{ amount: 30, label: 'I can support others too' },
|
||||||
})
|
{ amount: 50, label: 'I want to sponsor multiple members' },
|
||||||
|
])
|
||||||
it('returns annual amount for $50 tier', () => {
|
})
|
||||||
expect(getTierAmount({ amount: 50 }, 'annual')).toBe(500)
|
})
|
||||||
})
|
|
||||||
|
describe('requiresPayment', () => {
|
||||||
it('defaults cadence to monthly', () => {
|
it('returns false for 0', () => {
|
||||||
expect(getTierAmount({ amount: 15 })).toBe(15)
|
expect(requiresPayment(0)).toBe(false)
|
||||||
|
})
|
||||||
|
it('returns true for any positive number', () => {
|
||||||
|
expect(requiresPayment(1)).toBe(true)
|
||||||
|
expect(requiresPayment(15)).toBe(true)
|
||||||
|
})
|
||||||
|
it('returns false for null, undefined, NaN', () => {
|
||||||
|
expect(requiresPayment(null)).toBe(false)
|
||||||
|
expect(requiresPayment(undefined)).toBe(false)
|
||||||
|
expect(requiresPayment(NaN)).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('isValidContributionAmount', () => {
|
||||||
|
it('accepts non-negative integers', () => {
|
||||||
|
expect(isValidContributionAmount(0)).toBe(true)
|
||||||
|
expect(isValidContributionAmount(15)).toBe(true)
|
||||||
|
expect(isValidContributionAmount(9999)).toBe(true)
|
||||||
|
})
|
||||||
|
it('rejects negatives', () => {
|
||||||
|
expect(isValidContributionAmount(-1)).toBe(false)
|
||||||
|
})
|
||||||
|
it('rejects non-integers and non-numbers', () => {
|
||||||
|
expect(isValidContributionAmount(1.5)).toBe(false)
|
||||||
|
expect(isValidContributionAmount('15')).toBe(false)
|
||||||
|
expect(isValidContributionAmount(NaN)).toBe(false)
|
||||||
|
expect(isValidContributionAmount(null)).toBe(false)
|
||||||
|
expect(isValidContributionAmount(undefined)).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('getGuidanceLabel', () => {
|
||||||
|
it('returns the exact preset label when amount matches a preset', () => {
|
||||||
|
expect(getGuidanceLabel(0)).toBe('I need support right now')
|
||||||
|
expect(getGuidanceLabel(5)).toBe('I can contribute')
|
||||||
|
expect(getGuidanceLabel(15)).toBe('I can sustain the community')
|
||||||
|
expect(getGuidanceLabel(30)).toBe('I can support others too')
|
||||||
|
expect(getGuidanceLabel(50)).toBe('I want to sponsor multiple members')
|
||||||
|
})
|
||||||
|
it('returns the nearest-lower preset label for between-preset values', () => {
|
||||||
|
expect(getGuidanceLabel(7)).toBe('I can contribute')
|
||||||
|
expect(getGuidanceLabel(14)).toBe('I can contribute')
|
||||||
|
expect(getGuidanceLabel(40)).toBe('I can support others too')
|
||||||
|
expect(getGuidanceLabel(100)).toBe('I want to sponsor multiple members')
|
||||||
|
})
|
||||||
|
it('coerces string input', () => {
|
||||||
|
expect(getGuidanceLabel('15')).toBe('I can sustain the community')
|
||||||
|
})
|
||||||
|
it('returns null for invalid input', () => {
|
||||||
|
expect(getGuidanceLabel(-1)).toBeNull()
|
||||||
|
expect(getGuidanceLabel(NaN)).toBeNull()
|
||||||
|
expect(getGuidanceLabel('abc')).toBeNull()
|
||||||
|
expect(getGuidanceLabel(null)).toBeNull()
|
||||||
|
expect(getGuidanceLabel(undefined)).toBeNull()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue