From 62c606b30af2d3b3a6bbcb4b81fe9cfb5000291b Mon Sep 17 00:00:00 2001 From: Jennie Robinson Faber Date: Sun, 19 Apr 2026 18:07:43 +0100 Subject: [PATCH] feat(contributions): rewrite client config as preset-based helpers --- app/config/contributions.js | 38 +++++++-------- tests/client/contributions.test.js | 77 ++++++++++++++++++++++++++++++ 2 files changed, 96 insertions(+), 19 deletions(-) create mode 100644 tests/client/contributions.test.js diff --git a/app/config/contributions.js b/app/config/contributions.js index 985de53..7d8cab2 100644 --- a/app/config/contributions.js +++ b/app/config/contributions.js @@ -1,22 +1,22 @@ -// Central configuration for Ghost Guild Contribution Levels -export const CONTRIBUTION_TIERS = { - FREE: { value: "0", amount: 0, label: "$0 - I need support right now", tier: "free" }, - 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" }, - ADVOCATE: { value: "30", amount: 30, label: "$30 - I can support others too", tier: "advocate" }, - CHAMPION: { value: "50", amount: 50, label: "$50 - I want to sponsor multiple members", tier: "champion" }, -} +// Guidance presets for the contribution amount input. +// These are NOT tiers — just suggested amounts with matching guidance copy. +export const CONTRIBUTION_PRESETS = [ + { amount: 0, label: "I need support right now" }, + { 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 getContributionOptions = () => Object.values(CONTRIBUTION_TIERS) -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 const requiresPayment = (amount) => Number(amount) > 0 -export function getTierAmount(tier, cadence = 'monthly') { - const base = parseFloat(tier.amount) - return cadence === 'annual' ? base * 10 : base +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].reverse().find(p => p.amount <= n) + return match?.label ?? null } diff --git a/tests/client/contributions.test.js b/tests/client/contributions.test.js new file mode 100644 index 0000000..fff5db4 --- /dev/null +++ b/tests/client/contributions.test.js @@ -0,0 +1,77 @@ +import { describe, it, expect } from 'vitest' +import { + CONTRIBUTION_PRESETS, + requiresPayment, + isValidContributionAmount, + getGuidanceLabel, +} from '../../app/config/contributions.js' + +describe('CONTRIBUTION_PRESETS', () => { + it('exposes the five suggested amounts with guidance labels', () => { + expect(CONTRIBUTION_PRESETS).toEqual([ + { amount: 0, label: 'I need support right now' }, + { 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' }, + ]) + }) +}) + +describe('requiresPayment', () => { + it('returns false for 0', () => { + expect(requiresPayment(0)).toBe(false) + }) + it('returns true for any positive number', () => { + expect(requiresPayment(1)).toBe(true) + expect(requiresPayment(15)).toBe(true) + }) + it('coerces string numbers', () => { + expect(requiresPayment('15')).toBe(true) + expect(requiresPayment('0')).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() + }) +})