diff --git a/src/application/services/PaymentProcessor.ts b/src/application/services/PaymentProcessor.ts index 1c58673..138f9ae 100644 --- a/src/application/services/PaymentProcessor.ts +++ b/src/application/services/PaymentProcessor.ts @@ -121,7 +121,9 @@ export class PaymentProcessor { if (conversionResult.status === 'failed') { // Need to refund the onramp charge - await this.refundOnramp(onrampResult.details.chargeId); + if (onrampResult.details?.chargeId) { + await this.refundOnramp(onrampResult.details.chargeId); + } await this.updatePaymentStatus(paymentId, PaymentStatus.FAILED, `Currency conversion failed: ${conversionResult.error}`); return { @@ -140,7 +142,9 @@ export class PaymentProcessor { if (offrampResult.status === 'failed') { // Need to refund everything - await this.refundOnramp(onrampResult.details.chargeId); + if (onrampResult.details?.chargeId) { + await this.refundOnramp(onrampResult.details.chargeId); + } await this.updatePaymentStatus(paymentId, PaymentStatus.FAILED, `Offramp failed: ${offrampResult.error}`); return { diff --git a/src/application/services/PaymentService.ts b/src/application/services/PaymentService.ts index 0631034..5766ae8 100644 --- a/src/application/services/PaymentService.ts +++ b/src/application/services/PaymentService.ts @@ -7,6 +7,7 @@ import { ExchangeService } from './ExchangeService'; import { WebhookService } from './WebhookService'; import { logger } from '../../infrastructure/logger'; import { v4 as uuidv4 } from 'uuid'; +import { Decimal } from '@prisma/client/runtime/library'; export class PaymentService { private feeCalculator: FeeCalculator; @@ -36,19 +37,23 @@ export class PaymentService { // 2. Calculate fees const feeResult = await this.feeCalculator.calculateFees(request); - // 3. Calculate amounts - const sourceAmountAfterFees = request.sourceAmount - feeResult.totalFeeAmount; - const targetAmount = sourceAmountAfterFees * exchangeRate; - const netAmount = targetAmount; // Net amount recipient receives + // 3. Calculate amounts using Decimal for precision + const sourceAmountDecimal = new Decimal(request.sourceAmount); + const feeAmountDecimal = new Decimal(feeResult.totalFeeAmount); + const exchangeRateDecimal = new Decimal(exchangeRate); + + const sourceAmountAfterFees = sourceAmountDecimal.minus(feeAmountDecimal); + const targetAmountDecimal = sourceAmountAfterFees.times(exchangeRateDecimal); + const netAmount = targetAmountDecimal; // Net amount recipient receives const quote: PaymentQuote = { sourceAmount: request.sourceAmount, sourceCurrency: request.sourceCurrency, - targetAmount: Number(targetAmount.toFixed(2)), + targetAmount: targetAmountDecimal.toDecimalPlaces(2).toNumber(), targetCurrency: request.targetCurrency, exchangeRate: exchangeRate, totalFees: feeResult.totalFeeAmount, - netAmount: Number(netAmount.toFixed(2)), + netAmount: netAmount.toDecimalPlaces(2).toNumber(), expiresAt: new Date(Date.now() + 15 * 60 * 1000) // Expires in 15 minutes }; diff --git a/src/infrastructure/crypto.ts b/src/infrastructure/crypto.ts index ac727f9..59ade0a 100644 --- a/src/infrastructure/crypto.ts +++ b/src/infrastructure/crypto.ts @@ -141,14 +141,25 @@ export class WebhookSecurity { // Block internal/private IPs in production if (process.env.NODE_ENV === 'production') { const hostname = parsed.hostname; - - // Block localhost, private IPs, etc. + + // Block localhost and loopback addresses (IPv4 and IPv6) if ( hostname === 'localhost' || - hostname.startsWith('127.') || - hostname.startsWith('192.168.') || - hostname.startsWith('10.') || - hostname.startsWith('172.') + hostname === '::1' || // IPv6 loopback + hostname.startsWith('127.') || // IPv4 loopback + hostname.startsWith('192.168.') || // Private IPv4 + hostname.startsWith('10.') || // Private IPv4 + hostname.startsWith('172.16.') || // Private IPv4 (172.16.0.0/12) + hostname.startsWith('172.17.') || + hostname.startsWith('172.18.') || + hostname.startsWith('172.19.') || + hostname.startsWith('172.2') || // 172.20-29 + hostname.startsWith('172.30.') || + hostname.startsWith('172.31.') || + hostname.startsWith('169.254.') || // Link-local IPv4 + hostname.startsWith('fc00:') || // IPv6 Unique Local Address + hostname.startsWith('fd00:') || // IPv6 Unique Local Address + hostname.startsWith('fe80:') // IPv6 Link-local ) { return false; } diff --git a/src/infrastructure/providers/MockOffRampProvider.ts b/src/infrastructure/providers/MockOffRampProvider.ts index 66ca577..91e9279 100644 --- a/src/infrastructure/providers/MockOffRampProvider.ts +++ b/src/infrastructure/providers/MockOffRampProvider.ts @@ -159,8 +159,8 @@ export class MockOfframpProvider { await this.delay(300 + Math.random() * 500); // Mock transfer data - const statuses = ['pending', 'processing', 'completed', 'failed']; - const randomStatus = statuses[Math.floor(Math.random() * statuses.length)] as any; + const statuses: Array<'pending' | 'processing' | 'completed' | 'failed'> = ['pending', 'processing', 'completed', 'failed']; + const randomStatus = statuses[Math.floor(Math.random() * statuses.length)]; return { id: transferId, @@ -218,19 +218,30 @@ export class MockOfframpProvider { } }; - logger.info('Offramp webhook sent', { + logger.info('Offramp webhook sending', { eventId: webhookEvent.id, paymentId: data.paymentId, status: data.status, delay: webhookDelay }); - // TODO: Actually send HTTP request to our webhook endpoint - // await fetch(this.webhookUrl + '/webhooks/offramp', { - // method: 'POST', - // headers: { 'Content-Type': 'application/json' }, - // body: JSON.stringify(webhookEvent) - // }); + // Send HTTP request to our webhook endpoint + await fetch(this.webhookUrl + '/webhooks/offramp', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'User-Agent': 'OfframpProvider-Webhook/1.0' + }, + body: JSON.stringify(webhookEvent) + }).then(response => { + if (!response.ok) { + throw new Error(`Webhook delivery failed with status ${response.status}`); + } + logger.info('Offramp webhook delivered successfully', { + eventId: webhookEvent.id, + paymentId: data.paymentId + }); + }); } catch (error) { logger.error('Failed to send offramp webhook', { diff --git a/src/infrastructure/providers/MockStripProvider.ts b/src/infrastructure/providers/MockStripProvider.ts index d671b13..00285b2 100644 --- a/src/infrastructure/providers/MockStripProvider.ts +++ b/src/infrastructure/providers/MockStripProvider.ts @@ -203,18 +203,29 @@ export class MockStripeProvider { }; // In a real app, this would be an HTTP POST to our webhook endpoint - logger.info('Stripe webhook sent', { + logger.info('Stripe webhook sending', { eventId: webhookEvent.id, paymentId: data.paymentId, status: data.status }); - // TODO: Actually send HTTP request to our webhook endpoint - // await fetch(this.webhookUrl + '/webhooks/stripe', { - // method: 'POST', - // headers: { 'Content-Type': 'application/json' }, - // body: JSON.stringify(webhookEvent) - // }); + // Send HTTP request to our webhook endpoint + await fetch(this.webhookUrl + '/webhooks/stripe', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'User-Agent': 'Stripe-Webhook/1.0' + }, + body: JSON.stringify(webhookEvent) + }).then(response => { + if (!response.ok) { + throw new Error(`Webhook delivery failed with status ${response.status}`); + } + logger.info('Stripe webhook delivered successfully', { + eventId: webhookEvent.id, + paymentId: data.paymentId + }); + }); } catch (error) { logger.error('Failed to send Stripe webhook', { diff --git a/src/presentation/middleware/validation.ts b/src/presentation/middleware/validation.ts index efb33a1..5998cff 100644 --- a/src/presentation/middleware/validation.ts +++ b/src/presentation/middleware/validation.ts @@ -1,6 +1,7 @@ import { Request, Response, NextFunction } from 'express'; import { z } from 'zod'; import { logger } from '../../infrastructure/logger'; +import { ValidatedRequest } from '../types'; // Validation schemas export const createPaymentSchema = z.object({ @@ -37,9 +38,9 @@ export function validateRequest(schema: z.ZodSchema) { try { // Validate request body const validatedData = schema.parse(req.body); - - // Attach validated data to request - (req as any).validatedBody = validatedData; + + // Attach validated data to request (properly typed) + (req as ValidatedRequest).validatedBody = validatedData; logger.debug('Request validation successful', { endpoint: req.path, diff --git a/src/presentation/routes/admin.ts b/src/presentation/routes/admin.ts index edddd22..f7353c5 100644 --- a/src/presentation/routes/admin.ts +++ b/src/presentation/routes/admin.ts @@ -24,8 +24,16 @@ interface PaymentEvent { data: any; } -interface Fee { - amount: number; +interface PrismaFee { + id: string; + paymentId: string; + type: string; + amount: any; // Prisma Decimal + currency: string; + rate: any | null; + provider: string | null; + description: string | null; + createdAt: Date; } const router = express.Router(); @@ -192,7 +200,9 @@ router.get('/payments/:id/details', async (req: express.Request, res: express.Re })); // Calculate totals - const totalFees = payment.fees.reduce((sum: number, fee: any) => sum + Number(fee.amount), 0); + const totalFees = payment.fees.reduce((sum: number, fee: PrismaFee) => { + return sum + Number(fee.amount); + }, 0); const netAmount = Number(payment.targetAmount || 0) - totalFees; const response: ApiResponse = { diff --git a/src/presentation/routes/payments.ts b/src/presentation/routes/payments.ts index e1bbd6e..356500f 100644 --- a/src/presentation/routes/payments.ts +++ b/src/presentation/routes/payments.ts @@ -1,23 +1,23 @@ import express from 'express'; import { PaymentService } from '../../application/services/PaymentService'; -import { - validateCreatePayment, - validateGetQuote, - validateSupportedCurrencies +import { + validateCreatePayment, + validateGetQuote, + validateSupportedCurrencies } from '../middleware/validation'; import { logger } from '../../infrastructure/logger'; -import { CreatePaymentRequest, ApiResponse } from '../types'; +import { CreatePaymentRequest, ApiResponse, ValidatedRequest, PaymentQuoteRequest } from '../types'; const router = express.Router(); const paymentService = new PaymentService(); // POST /api/v1/quote - Get payment quote -router.post('/quote', +router.post('/quote', validateGetQuote, validateSupportedCurrencies, async (req: express.Request, res: express.Response) => { try { - const quoteRequest = (req as any).validatedBody; + const quoteRequest = (req as ValidatedRequest).validatedBody; const quote = await paymentService.createPaymentQuote(quoteRequest); @@ -53,7 +53,7 @@ router.post('/payments', validateSupportedCurrencies, async (req: express.Request, res: express.Response) => { try { - const paymentRequest: CreatePaymentRequest = (req as any).validatedBody; + const paymentRequest = (req as ValidatedRequest).validatedBody; const result = await paymentService.createPayment(paymentRequest); diff --git a/src/presentation/routes/webhook.ts b/src/presentation/routes/webhook.ts index d7408f0..05942f6 100644 --- a/src/presentation/routes/webhook.ts +++ b/src/presentation/routes/webhook.ts @@ -198,42 +198,51 @@ async function handleStripePaymentSucceeded(stripeEvent: any): Promise { }); // Update payment status and create event - await prisma.$transaction(async (tx: PrismaTransaction) => { - // Update payment - await tx.payment.update({ - where: { id: paymentId }, - data: { - externalReference: paymentIntent.id, - updatedAt: new Date() - } - }); + try { + await prisma.$transaction(async (tx: PrismaTransaction) => { + // Update payment + await tx.payment.update({ + where: { id: paymentId }, + data: { + externalReference: paymentIntent.id, + updatedAt: new Date() + } + }); - // Create event - await tx.paymentEvent.create({ - data: { - paymentId, - eventType: 'stripe_payment_succeeded', + // Create event + await tx.paymentEvent.create({ data: { - paymentIntentId: paymentIntent.id, - amount: paymentIntent.amount, - currency: paymentIntent.currency, - stripeEventId: stripeEvent.id - }, - source: 'stripe_webhook' - } + paymentId, + eventType: 'stripe_payment_succeeded', + data: { + paymentIntentId: paymentIntent.id, + amount: paymentIntent.amount, + currency: paymentIntent.currency, + stripeEventId: stripeEvent.id + }, + source: 'stripe_webhook' + } + }); }); - }); - // Send customer notification webhook - await webhookService.sendPaymentWebhook( - paymentId, - WebhookEventType.PAYMENT_PROCESSING, - { - step: 'onramp_completed', - provider: 'stripe', - externalReference: paymentIntent.id - } - ); + // Send customer notification webhook + await webhookService.sendPaymentWebhook( + paymentId, + WebhookEventType.PAYMENT_PROCESSING, + { + step: 'onramp_completed', + provider: 'stripe', + externalReference: paymentIntent.id + } + ); + } catch (transactionError) { + logger.error('Transaction failed in Stripe payment handling', { + stripeEventId: stripeEvent.id, + paymentId, + error: transactionError instanceof Error ? transactionError.message : transactionError + }); + throw transactionError; + } } catch (error) { logger.error('Failed to handle Stripe payment succeeded', { diff --git a/src/presentation/types/index.ts b/src/presentation/types/index.ts index 109d75c..ba16e30 100644 --- a/src/presentation/types/index.ts +++ b/src/presentation/types/index.ts @@ -1,4 +1,10 @@ import { IPayment, IFee } from '../../domain/interfaces'; +import { Request } from 'express'; + +// Extended Express Request with validated body +export interface ValidatedRequest extends Request { + validatedBody: T; +} // API Request Types export interface CreatePaymentRequest {