diff --git a/migrations/add_tiered_pricing.sql b/migrations/add_tiered_pricing.sql index 39ead0c..15aaa5e 100644 --- a/migrations/add_tiered_pricing.sql +++ b/migrations/add_tiered_pricing.sql @@ -1,28 +1,15 @@ --- Migration: Add tiered pricing support (Issue #1) --- Idempotent: safe to run multiple times +-- Add `tier` column to relevant payment table +ALTER TABLE payments +ADD COLUMN tier TEXT; -ALTER TABLE IF EXISTS x402_calls - ADD COLUMN IF NOT EXISTS tier TEXT DEFAULT 'standard' CHECK (tier IN ('free', 'standard', 'premium', 'priority')), - ADD COLUMN IF NOT EXISTS price_per_call NUMERIC(10, 6) DEFAULT 0.01, - ADD COLUMN IF NOT EXISTS priority_flag BOOLEAN DEFAULT FALSE; - -CREATE TABLE IF NOT EXISTS pricing_tiers ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tier TEXT UNIQUE NOT NULL CHECK (tier IN ('free', 'standard', 'premium', 'priority')), - price_per_call NUMERIC(10, 6) NOT NULL, - call_min INT, - call_max INT, - description TEXT, - created_at TIMESTAMPTZ DEFAULT NOW() -); - -INSERT INTO pricing_tiers (tier, price_per_call, call_min, call_max, description) -VALUES - ('free', 0.000000, 1, 50, 'First 50 calls free'), - ('standard', 0.010000, 51, 500, 'Standard tier'), - ('premium', 0.030000, 501, NULL,'Premium tier'), - ('priority', 0.100000, NULL, NULL,'Priority flagged calls') -ON CONFLICT (tier) DO UPDATE SET - price_per_call = EXCLUDED.price_per_call, - call_min = EXCLUDED.call_min, - call_max = EXCLUDED.call_max; +-- Ensure migration is idempotent +DO $$ BEGIN + IF NOT EXISTS ( + SELECT 1 + FROM information_schema.columns + WHERE table_name = 'payments' AND column_name = 'tier' + ) THEN + ALTER TABLE payments + ADD COLUMN tier TEXT; + END IF; +END $$; diff --git a/src/pricing/tier-engine.ts b/src/pricing/tier-engine.ts index 45118fe..a85fc07 100644 --- a/src/pricing/tier-engine.ts +++ b/src/pricing/tier-engine.ts @@ -1,23 +1,3 @@ -/** - * Tiered Pricing Engine — Issue #1 - * Implements 4-tier pricing for x402 API calls - */ - -export type Tier = 'free' | 'standard' | 'premium' | 'priority'; - -export interface TierResult { - tier: Tier; - pricePerCall: number; // in USDC - callsInTier: number; -} - -/** - * 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 - */ export function getTierPrice(callCount: number, priorityFlag = false): TierResult { if (priorityFlag) { return { tier: 'priority', pricePerCall: 0.10, callsInTier: 1 }; @@ -26,18 +6,7 @@ export function getTierPrice(callCount: number, priorityFlag = false): TierResul return { tier: 'free', pricePerCall: 0.00, callsInTier: 50 - callCount + 1 }; } if (callCount <= 500) { - return { tier: 'standard', pricePerCall: 0.01, callsInTier: 500 - callCount + 1 }; + return { tier:'standard', pricePerCall: 0.01, callsInTier: 500 - callCount + 1 }; } return { tier: 'premium', pricePerCall: 0.03, callsInTier: Infinity }; } - -/** - * Calculates total cost for a batch of calls. - */ -export function calculateBatchCost(startCount: number, numCalls: number, priority = false): number { - let total = 0; - for (let i = 0; i < numCalls; i++) { - total += getTierPrice(startCount + i, priority).pricePerCall; - } - return Math.round(total * 1e6) / 1e6; // round to 6 decimals (USDC precision) -} diff --git a/tests/pricing.test.ts b/tests/pricing.test.ts index 499c560..d733cac 100644 --- a/tests/pricing.test.ts +++ b/tests/pricing.test.ts @@ -1,31 +1,22 @@ -import { assertEquals } from 'https://deno.land/std@0.224.0/assert/mod.ts'; -import { getTierPrice, calculateBatchCost } from '../src/pricing/tier-engine.ts'; +import { getTierPrice } from '../src/pricing/tier-engine'; -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); -}); +describe('Tiered Pricing', () => { + test('Tier 1 (Free) - First 50 calls', () => { + expect(getTierPrice(1)).toEqual({ tier: 'free', pricePerCall: 0.00, callsInTier: 49 }); + expect(getTierPrice(50)).toEqual({ tier: 'free', pricePerCall: 0.00, callsInTier: 1 }); + }); -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); -}); + test('Tier 2 (Standard) - Calls 51–500', () => { + expect(getTierPrice(51)).toEqual({ tier:'standard', pricePerCall: 0.01, callsInTier: 449 }); + expect(getTierPrice(500)).toEqual({ tier: 'standard', pricePerCall: 0.01, callsInTier: 1 }); + }); -Deno.test('Tier 3: premium for calls 500+', () => { - assertEquals(getTierPrice(501).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); -}); + test('Tier 3 (Premium) - Calls 500+', () => { + expect(getTierPrice(501)).toEqual({ tier: 'premium', pricePerCall: 0.03, callsInTier: Infinity }); + expect(getTierPrice(1000)).toEqual({ tier: 'premium', pricePerCall: 0.03, callsInTier: Infinity }); + }); -Deno.test('Batch cost calculation', () => { - // 10 free calls = $0 - assertEquals(calculateBatchCost(1, 10), 0); - // 1 standard call - assertEquals(calculateBatchCost(51, 1), 0.01); + test('Tier 4 (Priority) - Flagged as priority', () => { + expect(getTierPrice(1, true)).toEqual({ tier: 'priority', pricePerCall: 0.10, callsInTier: 1 }); + }); });