diff --git a/src/pricing/tier-engine.ts b/src/pricing/tier-engine.ts index 45118fe..f8915be 100644 --- a/src/pricing/tier-engine.ts +++ b/src/pricing/tier-engine.ts @@ -1,9 +1,12 @@ /** * Tiered Pricing Engine — Issue #1 - * Implements 4-tier pricing for x402 API calls + * Implements 4-tier pricing for x402 API calls. + * + * Acceptance surface includes both camelCase and snake_case entry points: + * `getTierPrice` / `get_tier_price(call_count, priority_flag)`. */ -export type Tier = 'free' | 'standard' | 'premium' | 'priority'; +export type Tier = "free" | "standard" | "premium" | "priority"; export interface TierResult { tier: Tier; @@ -11,33 +14,62 @@ export interface TierResult { callsInTier: number; } +function normalizeCallCount(callCount: number): number { + if (!Number.isFinite(callCount)) { + throw new RangeError("callCount must be a finite number"); + } + // Treat non-positive counts as "first call" so free tier applies. + if (callCount < 1) return 1; + return Math.floor(callCount); +} + /** * Returns the price per call based on total call count and priority flag. * - Tier 1 (Free): calls 1–50 → $0.00 - * - Tier 2 (Standard): calls 51–500 → $0.01 - * - Tier 3 (Premium): calls 500+ → $0.03 - * - Tier 4 (Priority): priority=true → $0.10 + * - Tier 2 (Standard): calls 51–500 → $0.01 + * - Tier 3 (Premium): calls 501+ → $0.03 + * - Tier 4 (Priority): priority=true → $0.10 (overrides count tiers) */ -export function getTierPrice(callCount: number, priorityFlag = false): TierResult { +export function getTierPrice( + callCount: number, + priorityFlag = false, +): TierResult { + const n = normalizeCallCount(callCount); if (priorityFlag) { - return { tier: 'priority', pricePerCall: 0.10, callsInTier: 1 }; + return { tier: "priority", pricePerCall: 0.1, callsInTier: 1 }; } - if (callCount <= 50) { - return { tier: 'free', pricePerCall: 0.00, callsInTier: 50 - callCount + 1 }; + if (n <= 50) { + return { tier: "free", pricePerCall: 0.0, callsInTier: 50 - n + 1 }; } - if (callCount <= 500) { - return { tier: 'standard', pricePerCall: 0.01, callsInTier: 500 - callCount + 1 }; + if (n <= 500) { + return { tier: "standard", pricePerCall: 0.01, callsInTier: 500 - n + 1 }; } - return { tier: 'premium', pricePerCall: 0.03, callsInTier: Infinity }; + return { tier: "premium", pricePerCall: 0.03, callsInTier: Infinity }; +} + +/** Snake_case alias required by issue #1 acceptance criteria. */ +export function get_tier_price( + call_count: number, + priority_flag = false, +): TierResult { + return getTierPrice(call_count, priority_flag); } /** - * Calculates total cost for a batch of calls. + * Calculates total cost for a batch of calls starting at `startCount`. */ -export function calculateBatchCost(startCount: number, numCalls: number, priority = false): number { +export function calculateBatchCost( + startCount: number, + numCalls: number, + priority = false, +): number { + if (!Number.isFinite(numCalls) || numCalls < 0) { + throw new RangeError("numCalls must be a non-negative finite number"); + } let total = 0; - for (let i = 0; i < numCalls; i++) { + const n = Math.floor(numCalls); + for (let i = 0; i < n; i++) { total += getTierPrice(startCount + i, priority).pricePerCall; } - return Math.round(total * 1e6) / 1e6; // round to 6 decimals (USDC precision) + return Math.round(total * 1e6) / 1e6; // USDC precision } diff --git a/tests/pricing.test.ts b/tests/pricing.test.ts index 499c560..85c9e9b 100644 --- a/tests/pricing.test.ts +++ b/tests/pricing.test.ts @@ -1,31 +1,58 @@ -import { assertEquals } from 'https://deno.land/std@0.224.0/assert/mod.ts'; -import { getTierPrice, calculateBatchCost } from '../src/pricing/tier-engine.ts'; +import { assertEquals, assertThrows } from "https://deno.land/std@0.224.0/assert/mod.ts"; +import { + calculateBatchCost, + get_tier_price, + getTierPrice, +} from "../src/pricing/tier-engine.ts"; -Deno.test('Tier 1: free for first 50 calls', () => { - assertEquals(getTierPrice(1).tier, 'free'); - assertEquals(getTierPrice(50).tier, 'free'); - assertEquals(getTierPrice(1).pricePerCall, 0.00); +Deno.test("Tier 1: free for first 50 calls", () => { + assertEquals(getTierPrice(1).tier, "free"); + assertEquals(getTierPrice(50).tier, "free"); + assertEquals(getTierPrice(1).pricePerCall, 0.0); + assertEquals(getTierPrice(50).pricePerCall, 0.0); }); -Deno.test('Tier 2: standard for calls 51-500', () => { - assertEquals(getTierPrice(51).tier, 'standard'); - assertEquals(getTierPrice(500).tier, 'standard'); +Deno.test("Tier 2: standard for calls 51-500", () => { + assertEquals(getTierPrice(51).tier, "standard"); + assertEquals(getTierPrice(500).tier, "standard"); assertEquals(getTierPrice(51).pricePerCall, 0.01); + assertEquals(getTierPrice(500).pricePerCall, 0.01); }); -Deno.test('Tier 3: premium for calls 500+', () => { - assertEquals(getTierPrice(501).tier, 'premium'); +Deno.test("Tier 3: premium for calls 501+", () => { + assertEquals(getTierPrice(501).tier, "premium"); + assertEquals(getTierPrice(10_000).tier, "premium"); assertEquals(getTierPrice(501).pricePerCall, 0.03); }); -Deno.test('Tier 4: priority flag overrides all', () => { - assertEquals(getTierPrice(1, true).tier, 'priority'); - assertEquals(getTierPrice(1000, true).pricePerCall, 0.10); +Deno.test("Tier 4: priority flag overrides all count tiers", () => { + assertEquals(getTierPrice(1, true).tier, "priority"); + assertEquals(getTierPrice(1000, true).pricePerCall, 0.1); + assertEquals(get_tier_price(25, true).tier, "priority"); }); -Deno.test('Batch cost calculation', () => { - // 10 free calls = $0 +Deno.test("snake_case get_tier_price matches getTierPrice", () => { + for (const n of [1, 50, 51, 500, 501]) { + assertEquals(get_tier_price(n), getTierPrice(n)); + assertEquals(get_tier_price(n, true), getTierPrice(n, true)); + } +}); + +Deno.test("boundaries: non-positive callCount clamps into free tier", () => { + assertEquals(getTierPrice(0).tier, "free"); + assertEquals(getTierPrice(-3).tier, "free"); + assertEquals(get_tier_price(0).pricePerCall, 0); +}); + +Deno.test("invalid callCount throws", () => { + assertThrows(() => getTierPrice(Number.NaN), RangeError); + assertThrows(() => get_tier_price(Number.POSITIVE_INFINITY), RangeError); +}); + +Deno.test("Batch cost calculation", () => { assertEquals(calculateBatchCost(1, 10), 0); - // 1 standard call assertEquals(calculateBatchCost(51, 1), 0.01); + // 2 free + 2 standard across the 50→51 boundary + assertEquals(calculateBatchCost(49, 4), 0.02); + assertEquals(calculateBatchCost(1, 3, true), 0.3); });