From 7fb45570a732d105c650507957cce95b994b5248 Mon Sep 17 00:00:00 2001 From: Bas van den Aakster Date: Mon, 15 Sep 2025 21:07:22 +0200 Subject: [PATCH] chore: Remove unused utility modules and related test files - Remove currency, logger, validation utilities, and base/test provider logic - Delete associated tests and TypeScript definitions for deprecated modules - Clean up exports in `src/utils` to reflect module removals --- package.json | 2 +- src/__tests__/test-provider.test.ts | 283 ---------------------------- src/providers/base/provider.ts | 63 ------- src/providers/test/provider.ts | 225 ---------------------- src/utils/currency.ts | 130 ------------- src/utils/index.ts | 3 - src/utils/logger.ts | 113 ----------- src/utils/validation.ts | 181 ------------------ 8 files changed, 1 insertion(+), 999 deletions(-) delete mode 100644 src/__tests__/test-provider.test.ts delete mode 100644 src/providers/base/provider.ts delete mode 100644 src/providers/test/provider.ts delete mode 100644 src/utils/currency.ts delete mode 100644 src/utils/index.ts delete mode 100644 src/utils/logger.ts delete mode 100644 src/utils/validation.ts diff --git a/package.json b/package.json index bc6a46b..a60f513 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@xtr-dev/payload-billing", - "version": "0.1.1", + "version": "0.1.2", "description": "PayloadCMS plugin for billing and payment provider integrations with tracking and local testing", "license": "MIT", "type": "module", diff --git a/src/__tests__/test-provider.test.ts b/src/__tests__/test-provider.test.ts deleted file mode 100644 index 6572108..0000000 --- a/src/__tests__/test-provider.test.ts +++ /dev/null @@ -1,283 +0,0 @@ -import type { TestProviderConfig} from '../types'; - -import { TestPaymentProvider } from '../providers/test/provider' -import { PaymentStatus } from '../types' - -describe('TestPaymentProvider', () => { - let provider: TestPaymentProvider - let config: TestProviderConfig - - beforeEach(() => { - config = { - autoComplete: true, - defaultDelay: 0, - enabled: true, - } - provider = new TestPaymentProvider(config) - }) - - afterEach(() => { - provider.clearStoredData() - }) - - describe('createPayment', () => { - it('should create a payment with succeeded status when autoComplete is true', async () => { - const payment = await provider.createPayment({ - amount: 2000, - currency: 'USD', - description: 'Test payment', - }) - - expect(payment).toMatchObject({ - amount: 2000, - currency: 'USD', - description: 'Test payment', - provider: 'test', - status: 'succeeded', - }) - expect(payment.id).toBeDefined() - expect(payment.createdAt).toBeDefined() - expect(payment.updatedAt).toBeDefined() - expect(payment.providerData?.testMode).toBe(true) - }) - - it('should create a payment with pending status when autoComplete is false', async () => { - config.autoComplete = false - provider = new TestPaymentProvider(config) - - const payment = await provider.createPayment({ - amount: 1500, - currency: 'EUR', - }) - - expect(payment).toMatchObject({ - amount: 1500, - currency: 'EUR', - status: 'pending', - }) - }) - - it('should create a failed payment when simulateFailure is true', async () => { - const payment = await provider.createPayment({ - amount: 1000, - currency: 'USD', - metadata: { - test: { simulateFailure: true }, - }, - }) - - expect(payment.status).toBe('failed') - expect(payment.providerData?.simulatedFailure).toBe(true) - }) - - it('should apply delay when specified', async () => { - const startTime = Date.now() - - await provider.createPayment({ - amount: 1000, - currency: 'USD', - metadata: { - test: { delayMs: 100 }, - }, - }) - - const endTime = Date.now() - expect(endTime - startTime).toBeGreaterThanOrEqual(100) - }) - - it('should store payment data', async () => { - const payment = await provider.createPayment({ - amount: 2000, - currency: 'USD', - }) - - const stored = provider.getStoredPayment(payment.id) - expect(stored).toEqual(payment) - }) - }) - - describe('retrievePayment', () => { - it('should retrieve an existing payment', async () => { - const payment = await provider.createPayment({ - amount: 2000, - currency: 'USD', - }) - - const retrieved = await provider.retrievePayment(payment.id) - expect(retrieved).toEqual(payment) - }) - - it('should throw error for non-existent payment', async () => { - await expect(provider.retrievePayment('non-existent')).rejects.toThrow( - 'Payment non-existent not found' - ) - }) - }) - - describe('cancelPayment', () => { - it('should cancel a pending payment', async () => { - config.autoComplete = false - provider = new TestPaymentProvider(config) - - const payment = await provider.createPayment({ - amount: 2000, - currency: 'USD', - }) - - const canceled = await provider.cancelPayment(payment.id) - expect(canceled.status).toBe('canceled') - expect(canceled.updatedAt).not.toBe(payment.updatedAt) - }) - - it('should not cancel a succeeded payment', async () => { - const payment = await provider.createPayment({ - amount: 2000, - currency: 'USD', - }) - - await expect(provider.cancelPayment(payment.id)).rejects.toThrow( - 'Cannot cancel a succeeded payment' - ) - }) - - it('should throw error for non-existent payment', async () => { - await expect(provider.cancelPayment('non-existent')).rejects.toThrow( - 'Payment non-existent not found' - ) - }) - }) - - describe('refundPayment', () => { - it('should create a full refund for succeeded payment', async () => { - const payment = await provider.createPayment({ - amount: 2000, - currency: 'USD', - }) - - const refund = await provider.refundPayment(payment.id) - - expect(refund).toMatchObject({ - amount: 2000, - currency: 'USD', - paymentId: payment.id, - status: 'succeeded', - }) - expect(refund.id).toBeDefined() - expect(refund.createdAt).toBeDefined() - - // Check payment status is updated - const updatedPayment = await provider.retrievePayment(payment.id) - expect(updatedPayment.status).toBe('refunded') - }) - - it('should create a partial refund', async () => { - const payment = await provider.createPayment({ - amount: 2000, - currency: 'USD', - }) - - const refund = await provider.refundPayment(payment.id, 1000) - - expect(refund.amount).toBe(1000) - - // Check payment status is updated to partially_refunded - const updatedPayment = await provider.retrievePayment(payment.id) - expect(updatedPayment.status).toBe('partially_refunded') - }) - - it('should not refund a non-succeeded payment', async () => { - config.autoComplete = false - provider = new TestPaymentProvider(config) - - const payment = await provider.createPayment({ - amount: 2000, - currency: 'USD', - }) - - await expect(provider.refundPayment(payment.id)).rejects.toThrow( - 'Can only refund succeeded payments' - ) - }) - - it('should not refund more than payment amount', async () => { - const payment = await provider.createPayment({ - amount: 2000, - currency: 'USD', - }) - - await expect(provider.refundPayment(payment.id, 3000)).rejects.toThrow( - 'Refund amount cannot exceed payment amount' - ) - }) - }) - - describe('handleWebhook', () => { - it('should handle webhook event', async () => { - const mockRequest = { - text: () => Promise.resolve(JSON.stringify({ - type: 'payment.succeeded', - data: { paymentId: 'test_pay_123' } - })) - } as Request - - const event = await provider.handleWebhook(mockRequest) - - expect(event).toMatchObject({ - type: 'payment.succeeded', - data: { paymentId: 'test_pay_123' }, - provider: 'test', - verified: true, - }) - expect(event.id).toBeDefined() - }) - - it('should throw error for invalid JSON', async () => { - const mockRequest = { - text: () => Promise.resolve('invalid json') - } as Request - - await expect(provider.handleWebhook(mockRequest)).rejects.toThrow( - 'Invalid JSON in webhook body' - ) - }) - - it('should throw error when provider is disabled', async () => { - config.enabled = false - provider = new TestPaymentProvider(config) - - const mockRequest = { - text: () => Promise.resolve('{}') - } as Request - - await expect(provider.handleWebhook(mockRequest)).rejects.toThrow( - 'Test provider is not enabled' - ) - }) - }) - - describe('data management', () => { - it('should clear all stored data', async () => { - await provider.createPayment({ amount: 1000, currency: 'USD' }) - - expect(provider.getAllPayments()).toHaveLength(1) - - provider.clearStoredData() - - expect(provider.getAllPayments()).toHaveLength(0) - expect(provider.getAllRefunds()).toHaveLength(0) - }) - - it('should return all payments and refunds', async () => { - const payment1 = await provider.createPayment({ amount: 1000, currency: 'USD' }) - const payment2 = await provider.createPayment({ amount: 2000, currency: 'EUR' }) - const refund = await provider.refundPayment(payment1.id) - - const payments = provider.getAllPayments() - const refunds = provider.getAllRefunds() - - expect(payments).toHaveLength(2) - expect(refunds).toHaveLength(1) - expect(refunds[0]).toEqual(refund) - }) - }) -}) \ No newline at end of file diff --git a/src/providers/base/provider.ts b/src/providers/base/provider.ts deleted file mode 100644 index 4a9ce44..0000000 --- a/src/providers/base/provider.ts +++ /dev/null @@ -1,63 +0,0 @@ -import type { CreatePaymentOptions, Payment, PaymentProvider, Refund, WebhookEvent } from '../../types' - -export abstract class BasePaymentProvider implements PaymentProvider { - abstract name: string - - protected formatAmount(amount: number, currency: string): number { - this.validateAmount(amount) - this.validateCurrency(currency) - return amount - } - protected log(level: 'error' | 'info' | 'warn', message: string, data?: Record): void { - const logData = { - message, - provider: this.name, - ...data, - } - - console[level](`[${this.name.toUpperCase()}]`, logData) - } - protected validateAmount(amount: number): void { - if (amount <= 0 || !Number.isInteger(amount)) { - throw new Error('Amount must be a positive integer in cents') - } - } - protected validateCurrency(currency: string): void { - if (!currency || currency.length !== 3) { - throw new Error('Currency must be a valid 3-letter ISO currency code') - } - } - abstract cancelPayment(id: string): Promise - - abstract createPayment(options: CreatePaymentOptions): Promise - - abstract handleWebhook(request: Request, signature?: string): Promise - - abstract refundPayment(id: string, amount?: number): Promise - - abstract retrievePayment(id: string): Promise -} - -export function createProviderRegistry() { - const providers = new Map() - - return { - register(provider: PaymentProvider): void { - providers.set(provider.name, provider) - }, - - get(name: string): PaymentProvider | undefined { - return providers.get(name) - }, - - getAll(): PaymentProvider[] { - return Array.from(providers.values()) - }, - - has(name: string): boolean { - return providers.has(name) - } - } -} - -export const providerRegistry = createProviderRegistry() \ No newline at end of file diff --git a/src/providers/test/provider.ts b/src/providers/test/provider.ts deleted file mode 100644 index 69b15c3..0000000 --- a/src/providers/test/provider.ts +++ /dev/null @@ -1,225 +0,0 @@ -import type { - CreatePaymentOptions, - Payment, - PaymentStatus, - Refund, - TestProviderConfig, - WebhookEvent -} from '../../types'; - -import { - RefundStatus -} from '../../types' -import { BasePaymentProvider } from '../base/provider' - -interface TestPaymentData { - delayMs?: number - failAfterMs?: number - simulateFailure?: boolean -} - -export class TestPaymentProvider extends BasePaymentProvider { - private config: TestProviderConfig - private payments = new Map() - private refunds = new Map() - name = 'test' - - constructor(config: TestProviderConfig) { - super() - this.config = config - } - - private sleep(ms: number): Promise { - return new Promise(resolve => setTimeout(resolve, ms)) - } - - async cancelPayment(id: string): Promise { - const payment = this.payments.get(id) - if (!payment) { - throw new Error(`Payment ${id} not found`) - } - - if (payment.status === 'succeeded') { - throw new Error('Cannot cancel a succeeded payment') - } - - const canceledPayment = { - ...payment, - status: 'canceled' as PaymentStatus, - updatedAt: new Date().toISOString() - } - - this.payments.set(id, canceledPayment) - - this.log('info', 'Payment canceled', { paymentId: id }) - - return canceledPayment - } - - clearStoredData(): void { - this.payments.clear() - this.refunds.clear() - this.log('info', 'Test data cleared') - } - - async createPayment(options: CreatePaymentOptions): Promise { - const testData = options.metadata?.test as TestPaymentData || {} - const delay = testData.delayMs ?? this.config.defaultDelay ?? 0 - - if (delay > 0) { - await this.sleep(delay) - } - - const shouldFail = testData.simulateFailure ?? - (this.config.simulateFailures && Math.random() < (this.config.failureRate ?? 0.1)) - - const paymentId = `test_pay_${Date.now()}_${Math.random().toString(36).substring(7)}` - - const payment: Payment = { - id: paymentId, - amount: options.amount, - createdAt: new Date().toISOString(), - currency: options.currency, - customer: options.customer, - description: options.description, - metadata: options.metadata, - provider: this.name, - providerData: { - autoCompleted: this.config.autoComplete, - delayApplied: delay, - simulatedFailure: shouldFail, - testMode: true - }, - status: shouldFail ? 'failed' : (this.config.autoComplete ? 'succeeded' : 'pending'), - updatedAt: new Date().toISOString() - } - - this.payments.set(paymentId, payment) - - this.log('info', 'Payment created', { - amount: options.amount, - currency: options.currency, - paymentId, - status: payment.status - }) - - // Simulate async status updates if configured - if (testData.failAfterMs && !shouldFail) { - setTimeout(() => { - const updatedPayment = { ...payment, status: 'failed' as PaymentStatus, updatedAt: new Date().toISOString() } - this.payments.set(paymentId, updatedPayment) - this.log('info', 'Payment failed after delay', { paymentId }) - }, testData.failAfterMs) - } - - return payment - } - - getAllPayments(): Payment[] { - return Array.from(this.payments.values()) - } - - getAllRefunds(): Refund[] { - return Array.from(this.refunds.values()) - } - - // Test-specific methods - getStoredPayment(id: string): Payment | undefined { - return this.payments.get(id) - } - - getStoredRefund(id: string): Refund | undefined { - return this.refunds.get(id) - } - - async handleWebhook(request: Request, signature?: string): Promise { - if (!this.config.enabled) { - throw new Error('Test provider is not enabled') - } - - // For test provider, we'll simulate webhook events - const body = await request.text() - let eventData: Record - - try { - eventData = JSON.parse(body) - } catch (error) { - throw new Error('Invalid JSON in webhook body') - } - - const event: WebhookEvent = { - id: `test_evt_${Date.now()}_${Math.random().toString(36).substring(7)}`, - type: (eventData.type as string) || 'payment.status_changed', - data: eventData, - provider: this.name, - verified: true // Test provider always considers webhooks verified - } - - this.log('info', 'Webhook received', { - type: event.type, - dataKeys: Object.keys(event.data), - eventId: event.id - }) - - return event - } - - async refundPayment(id: string, amount?: number): Promise { - const payment = this.payments.get(id) - if (!payment) { - throw new Error(`Payment ${id} not found`) - } - - if (payment.status !== 'succeeded') { - throw new Error('Can only refund succeeded payments') - } - - const refundAmount = amount ?? payment.amount - if (refundAmount > payment.amount) { - throw new Error('Refund amount cannot exceed payment amount') - } - - const refundId = `test_ref_${Date.now()}_${Math.random().toString(36).substring(7)}` - - const refund: Refund = { - id: refundId, - amount: refundAmount, - createdAt: new Date().toISOString(), - currency: payment.currency, - paymentId: id, - providerData: { - autoCompleted: this.config.autoComplete, - testMode: true - }, - status: this.config.autoComplete ? 'succeeded' : 'pending' - } - - this.refunds.set(refundId, refund) - - // Update payment status - const newPaymentStatus: PaymentStatus = refundAmount === payment.amount ? 'refunded' : 'partially_refunded' - const updatedPayment = { - ...payment, - status: newPaymentStatus, - updatedAt: new Date().toISOString() - } - this.payments.set(id, updatedPayment) - - this.log('info', 'Refund created', { - amount: refundAmount, - paymentId: id, - refundId, - status: refund.status - }) - - return refund - } - - async retrievePayment(id: string): Promise { - const payment = this.payments.get(id) - if (!payment) { - throw new Error(`Payment ${id} not found`) - } - return payment - } -} \ No newline at end of file diff --git a/src/utils/currency.ts b/src/utils/currency.ts deleted file mode 100644 index 820f5f7..0000000 --- a/src/utils/currency.ts +++ /dev/null @@ -1,130 +0,0 @@ -/** - * Currency utility functions for payment processing - */ - -// Common currency configurations -export const CURRENCY_CONFIG = { - AUD: { name: 'Australian Dollar', decimals: 2, symbol: 'A$' }, - CAD: { name: 'Canadian Dollar', decimals: 2, symbol: 'C$' }, - CHF: { name: 'Swiss Franc', decimals: 2, symbol: 'Fr' }, - DKK: { name: 'Danish Krone', decimals: 2, symbol: 'kr' }, - EUR: { name: 'Euro', decimals: 2, symbol: '€' }, - GBP: { name: 'British Pound', decimals: 2, symbol: '£' }, - JPY: { name: 'Japanese Yen', decimals: 0, symbol: '¥' }, - NOK: { name: 'Norwegian Krone', decimals: 2, symbol: 'kr' }, - SEK: { name: 'Swedish Krona', decimals: 2, symbol: 'kr' }, - USD: { name: 'US Dollar', decimals: 2, symbol: '$' }, -} as const - -export type SupportedCurrency = keyof typeof CURRENCY_CONFIG - -/** - * Validates if a currency code is supported - */ -export function isSupportedCurrency(currency: string): currency is SupportedCurrency { - return currency in CURRENCY_CONFIG -} - -/** - * Validates currency format (3-letter ISO code) - */ -export function isValidCurrencyCode(currency: string): boolean { - return /^[A-Z]{3}$/.test(currency) -} - -/** - * Converts amount from cents to major currency unit - */ -export function fromCents(amount: number, currency: string): number { - if (!isValidCurrencyCode(currency)) { - throw new Error(`Invalid currency code: ${currency}`) - } - - const config = CURRENCY_CONFIG[currency as SupportedCurrency] - if (!config) { - // Default to 2 decimals for unknown currencies - return amount / 100 - } - - return config.decimals === 0 ? amount : amount / Math.pow(10, config.decimals) -} - -/** - * Converts amount from major currency unit to cents - */ -export function toCents(amount: number, currency: string): number { - if (!isValidCurrencyCode(currency)) { - throw new Error(`Invalid currency code: ${currency}`) - } - - const config = CURRENCY_CONFIG[currency as SupportedCurrency] - if (!config) { - // Default to 2 decimals for unknown currencies - return Math.round(amount * 100) - } - - return config.decimals === 0 - ? Math.round(amount) - : Math.round(amount * Math.pow(10, config.decimals)) -} - -/** - * Formats amount for display with currency symbol - */ -export function formatAmount(amount: number, currency: string, options?: { - showCode?: boolean - showSymbol?: boolean -}): string { - const { showCode = false, showSymbol = true } = options || {} - - if (!isValidCurrencyCode(currency)) { - throw new Error(`Invalid currency code: ${currency}`) - } - - const majorAmount = fromCents(amount, currency) - const config = CURRENCY_CONFIG[currency as SupportedCurrency] - - let formatted = majorAmount.toFixed(config?.decimals ?? 2) - - if (showSymbol && config?.symbol) { - formatted = `${config.symbol}${formatted}` - } - - if (showCode) { - formatted += ` ${currency}` - } - - return formatted -} - -/** - * Gets currency information - */ -export function getCurrencyInfo(currency: string) { - if (!isValidCurrencyCode(currency)) { - throw new Error(`Invalid currency code: ${currency}`) - } - - return CURRENCY_CONFIG[currency as SupportedCurrency] || { - name: currency, - decimals: 2, - symbol: currency - } -} - -/** - * Validates amount is positive and properly formatted - */ -export function validateAmount(amount: number): void { - if (!Number.isFinite(amount)) { - throw new Error('Amount must be a finite number') - } - - if (amount <= 0) { - throw new Error('Amount must be positive') - } - - if (!Number.isInteger(amount)) { - throw new Error('Amount must be an integer (in cents)') - } -} \ No newline at end of file diff --git a/src/utils/index.ts b/src/utils/index.ts deleted file mode 100644 index 7573b56..0000000 --- a/src/utils/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './currency' -export * from './logger' -export * from './validation' \ No newline at end of file diff --git a/src/utils/logger.ts b/src/utils/logger.ts deleted file mode 100644 index d8be8a7..0000000 --- a/src/utils/logger.ts +++ /dev/null @@ -1,113 +0,0 @@ -/** - * Structured logging utilities for the billing plugin - */ - -export type LogLevel = 'debug' | 'error' | 'info' | 'warn' - -export interface LogContext { - [key: string]: unknown - amount?: number - currency?: string - customerId?: string - invoiceId?: string - paymentId?: string - provider?: string - refundId?: string - webhookId?: string -} - -export interface Logger { - debug(message: string, context?: LogContext): void - error(message: string, context?: LogContext): void - info(message: string, context?: LogContext): void - warn(message: string, context?: LogContext): void -} - -/** - * Creates a structured logger with consistent formatting - */ -export function createLogger(namespace: string = 'BILLING'): Logger { - const log = (level: LogLevel, message: string, context: LogContext = {}) => { - const timestamp = new Date().toISOString() - const logData = { - level: level.toUpperCase(), - message, - namespace, - timestamp, - ...context, - } - - // Use console methods based on log level - const consoleMethod = console[level] || console.log - consoleMethod(`[${namespace}] ${message}`, logData) - } - - return { - debug: (message: string, context?: LogContext) => log('debug', message, context), - error: (message: string, context?: LogContext) => log('error', message, context), - info: (message: string, context?: LogContext) => log('info', message, context), - warn: (message: string, context?: LogContext) => log('warn', message, context), - } -} - -/** - * Default logger instance for the plugin - */ -export const logger = createLogger('BILLING') - -/** - * Creates a provider-specific logger - */ -export function createProviderLogger(providerName: string): Logger { - return createLogger(`BILLING:${providerName.toUpperCase()}`) -} - -/** - * Log payment operations with consistent structure - */ -export function logPaymentOperation( - operation: string, - paymentId: string, - provider: string, - context?: LogContext -) { - logger.info(`Payment ${operation}`, { - operation, - paymentId, - provider, - ...context, - }) -} - -/** - * Log webhook events with consistent structure - */ -export function logWebhookEvent( - provider: string, - eventType: string, - webhookId: string, - context?: LogContext -) { - logger.info(`Webhook received`, { - eventType, - provider, - webhookId, - ...context, - }) -} - -/** - * Log errors with consistent structure - */ -export function logError( - error: Error, - operation: string, - context?: LogContext -) { - logger.error(`Operation failed: ${operation}`, { - error: error.message, - operation, - stack: error.stack, - ...context, - }) -} \ No newline at end of file diff --git a/src/utils/validation.ts b/src/utils/validation.ts deleted file mode 100644 index fa936b6..0000000 --- a/src/utils/validation.ts +++ /dev/null @@ -1,181 +0,0 @@ -/** - * Validation utilities for billing data - */ - -import { z } from 'zod' - -import { isValidCurrencyCode } from './currency' - -/** - * Zod schema for payment creation options - */ -export const createPaymentSchema = z.object({ - amount: z.number().int().positive('Amount must be positive').min(1, 'Amount must be at least 1 cent'), - cancelUrl: z.string().url('Invalid cancel URL').optional(), - currency: z.string().length(3, 'Currency must be 3 characters').regex(/^[A-Z]{3}$/, 'Currency must be uppercase'), - customer: z.string().optional(), - description: z.string().max(500, 'Description too long').optional(), - metadata: z.record(z.unknown()).optional(), - returnUrl: z.string().url('Invalid return URL').optional(), -}) - -/** - * Zod schema for customer data - */ -export const customerSchema = z.object({ - name: z.string().max(100, 'Name too long').optional(), - address: z.object({ - city: z.string().max(50).optional(), - country: z.string().length(2, 'Country must be 2 characters').regex(/^[A-Z]{2}$/, 'Country must be uppercase').optional(), - line1: z.string().max(100).optional(), - line2: z.string().max(100).optional(), - postal_code: z.string().max(20).optional(), - state: z.string().max(50).optional(), - }).optional(), - email: z.string().email('Invalid email address').optional(), - metadata: z.record(z.unknown()).optional(), - phone: z.string().max(20, 'Phone number too long').optional(), -}) - -/** - * Zod schema for invoice items - */ -export const invoiceItemSchema = z.object({ - description: z.string().min(1, 'Description is required').max(200, 'Description too long'), - quantity: z.number().int().positive('Quantity must be positive'), - unitAmount: z.number().int().min(0, 'Unit amount must be non-negative'), -}) - -/** - * Zod schema for invoice creation - */ -export const invoiceSchema = z.object({ - currency: z.string().length(3).regex(/^[A-Z]{3}$/), - customer: z.string().min(1, 'Customer is required'), - dueDate: z.string().datetime().optional(), - items: z.array(invoiceItemSchema).min(1, 'At least one item is required'), - metadata: z.record(z.unknown()).optional(), - notes: z.string().max(1000).optional(), - taxAmount: z.number().int().min(0).default(0), -}) - -/** - * Validates payment creation data - */ -export function validateCreatePayment(data: unknown) { - const result = createPaymentSchema.safeParse(data) - if (!result.success) { - throw new Error(`Invalid payment data: ${result.error.issues.map(i => i.message).join(', ')}`) - } - - // Additional currency validation - if (!isValidCurrencyCode(result.data.currency)) { - throw new Error(`Unsupported currency: ${result.data.currency}`) - } - - return result.data -} - -/** - * Validates customer data - */ -export function validateCustomer(data: unknown) { - const result = customerSchema.safeParse(data) - if (!result.success) { - throw new Error(`Invalid customer data: ${result.error.issues.map(i => i.message).join(', ')}`) - } - return result.data -} - -/** - * Validates invoice data - */ -export function validateInvoice(data: unknown) { - const result = invoiceSchema.safeParse(data) - if (!result.success) { - throw new Error(`Invalid invoice data: ${result.error.issues.map(i => i.message).join(', ')}`) - } - - // Additional currency validation - if (!isValidCurrencyCode(result.data.currency)) { - throw new Error(`Unsupported currency: ${result.data.currency}`) - } - - return result.data -} - -/** - * Validates webhook signature format - */ -export function validateWebhookSignature(signature: string, provider: string): void { - if (!signature) { - throw new Error(`Missing webhook signature for ${provider}`) - } - - switch (provider) { - case 'mollie': - if (signature.length < 32) { - throw new Error('Invalid Mollie webhook signature length') - } - break - case 'stripe': - if (!signature.startsWith('t=')) { - throw new Error('Invalid Stripe webhook signature format') - } - break - case 'test': - // Test provider accepts any signature - break - default: - throw new Error(`Unknown provider: ${provider}`) - } -} - -/** - * Validates payment provider name - */ -export function validateProviderName(provider: string): void { - const validProviders = ['stripe', 'mollie', 'test'] - if (!validProviders.includes(provider)) { - throw new Error(`Invalid provider: ${provider}. Must be one of: ${validProviders.join(', ')}`) - } -} - -/** - * Validates payment amount and currency combination - */ -export function validateAmountAndCurrency(amount: number, currency: string): void { - if (!Number.isInteger(amount) || amount <= 0) { - throw new Error('Amount must be a positive integer') - } - - if (!isValidCurrencyCode(currency)) { - throw new Error('Invalid currency code') - } - - // Validate minimum amounts for different currencies - const minimums: Record = { - EUR: 50, // €0.50 - GBP: 30, // £0.30 - JPY: 50, // ¥50 - USD: 50, // $0.50 - } - - const minimum = minimums[currency] || 50 - if (amount < minimum) { - throw new Error(`Amount too small for ${currency}. Minimum: ${minimum} cents`) - } -} - -/** - * Validates refund amount against original payment - */ -export function validateRefundAmount(refundAmount: number, paymentAmount: number): void { - if (!Number.isInteger(refundAmount) || refundAmount <= 0) { - throw new Error('Refund amount must be a positive integer') - } - - if (refundAmount > paymentAmount) { - throw new Error('Refund amount cannot exceed original payment amount') - } -} \ No newline at end of file