From cd8d6f1165bc9e30abfb1d496db3c02585733fcb Mon Sep 17 00:00:00 2001 From: idevakk <219866223+idevakk@users.noreply.github.com> Date: Thu, 4 Dec 2025 12:30:39 -0800 Subject: [PATCH] feat(payments): add error sanitization for Polar payment provider - Add sanitizePolarErrorMessage() method to convert API errors to user-friendly messages - Improve error logging with structured data instead of raw response bodies - Add validation error handling with field-specific messages - Remove sensitive information (emails, domains, UUIDs) from error messages - Update checkout, subscription, customer, and product error handling - Add status code-based error mapping for better user experience --- .../Payments/Providers/PolarProvider.php | 166 ++++++++++++++++-- 1 file changed, 156 insertions(+), 10 deletions(-) diff --git a/app/Services/Payments/Providers/PolarProvider.php b/app/Services/Payments/Providers/PolarProvider.php index 6670907..0ecfa3f 100644 --- a/app/Services/Payments/Providers/PolarProvider.php +++ b/app/Services/Payments/Providers/PolarProvider.php @@ -220,8 +220,19 @@ class PolarProvider implements PaymentProviderContract ]); if (! $response->successful()) { - $errorMessage = 'Polar checkout creation failed: '.$response->body(); - Log::error($errorMessage); + $statusCode = $response->status(); + $responseBody = $response->json(); + + // Log detailed error for debugging + Log::error('Polar checkout creation failed', [ + 'status_code' => $statusCode, + 'response' => $responseBody, + 'user_id' => $user->id, + 'plan_id' => $plan->id, + ]); + + // Create user-friendly error message without exposing sensitive data + $errorMessage = $this->sanitizePolarErrorMessage($responseBody, $statusCode); throw new \Exception($errorMessage); } @@ -460,8 +471,12 @@ class PolarProvider implements PaymentProviderContract $response = $this->makeAuthenticatedRequest('GET', '/subscriptions/'.$providerSubscriptionId); if (! $response->successful()) { - Log::error('Failed to retrieve Polar subscription: '.$response->body()); - throw new \Exception('Polar subscription not found: '.$response->status()); + Log::error('Failed to retrieve Polar subscription', [ + 'status_code' => $response->status(), + 'response' => $response->json(), + 'subscription_id' => $providerSubscriptionId, + ]); + throw new \Exception('Subscription not found. Please check your subscription details.'); } $polarSubscription = $response->json(); @@ -854,9 +869,13 @@ class PolarProvider implements PaymentProviderContract } } - $errorMessage = 'Failed to create Polar customer: '.$response->body(); - Log::error($errorMessage); - throw new \Exception($errorMessage); + Log::error('Failed to create Polar customer', [ + 'status_code' => $response->status(), + 'response' => $response->json(), + 'user_id' => $user->id, + ]); + + throw new \Exception('Failed to create customer account. Please try again or contact support.'); } $customer = $response->json(); @@ -964,9 +983,13 @@ class PolarProvider implements PaymentProviderContract $response = $this->makeAuthenticatedRequest('POST', '/products', $productData); if (! $response->successful()) { - $errorMessage = 'Failed to create Polar product: '.$response->body(); - Log::error($errorMessage); - throw new \Exception($errorMessage); + Log::error('Failed to create Polar product', [ + 'status_code' => $response->status(), + 'response' => $response->json(), + 'plan_id' => $plan->id, + ]); + + throw new \Exception('Failed to create payment plan. Please try again or contact support.'); } $product = $response->json(); @@ -1391,4 +1414,127 @@ class PolarProvider implements PaymentProviderContract Log::error('Import to Polar payments not implemented'); throw new \Exception('Import subscription data not implemented for Polar'); } + + /** + * Sanitize Polar API error messages to prevent exposing sensitive information + */ + private function sanitizePolarErrorMessage(array $responseBody, int $statusCode): string + { + // Handle specific error types with user-friendly messages + if (isset($responseBody['error'])) { + $errorType = $responseBody['error']; + + return match ($errorType) { + 'RequestValidationError' => $this->handleValidationError($responseBody), + 'AuthenticationError' => 'Payment service authentication failed. Please try again.', + 'InsufficientPermissions' => 'Insufficient permissions to process payment. Please contact support.', + 'ResourceNotFound' => 'Payment resource not found. Please try again.', + 'RateLimitExceeded' => 'Too many payment requests. Please wait and try again.', + 'PaymentRequired' => 'Payment required to complete this action.', + default => 'Payment processing failed. Please try again or contact support.', + }; + } + + // Handle validation errors in detail array + if (isset($responseBody['detail']) && is_array($responseBody['detail'])) { + return $this->handleValidationError($responseBody); + } + + // Generic error based on status code + return match ($statusCode) { + 400 => 'Invalid payment request. Please check your information and try again.', + 401 => 'Payment authentication failed. Please try again.', + 403 => 'Payment authorization failed. Please contact support.', + 404 => 'Payment service not available. Please try again.', + 429 => 'Too many payment requests. Please wait and try again.', + 500 => 'Payment service error. Please try again later.', + 502, 503, 504 => 'Payment service temporarily unavailable. Please try again later.', + default => 'Payment processing failed. Please try again or contact support.', + }; + } + + /** + * Handle validation errors and extract user-friendly messages + */ + private function handleValidationError(array $responseBody): string + { + if (! isset($responseBody['detail']) || ! is_array($responseBody['detail'])) { + return 'Invalid payment information provided. Please check your details and try again.'; + } + + $errors = []; + foreach ($responseBody['detail'] as $error) { + if (isset($error['msg']) && isset($error['loc'])) { + $field = $this->extractFieldName($error['loc']); + $message = $this->sanitizeValidationMessage($error['msg'], $field); + $errors[] = $message; + } + } + + if (empty($errors)) { + return 'Invalid payment information provided. Please check your details and try again.'; + } + + return implode(' ', array_unique($errors)); + } + + /** + * Extract field name from error location path + */ + private function extractFieldName(array $loc): string + { + if (empty($loc)) { + return 'field'; + } + + // Get the last element of the location array + $field = end($loc); + + // Convert to user-friendly field names + return match ($field) { + 'customer_email' => 'email address', + 'customer_name' => 'name', + 'products' => 'product selection', + 'product_id' => 'product selection', + 'product_price_id' => 'product selection', + 'success_url' => 'redirect settings', + 'cancel_url' => 'redirect settings', + default => strtolower(str_replace('_', ' ', $field)), + }; + } + + /** + * Sanitize validation message to remove sensitive information + */ + private function sanitizeValidationMessage(string $message, string $field): string + { + // Remove email addresses and other sensitive data from error messages + $sanitized = preg_replace('/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/', '[email]', $message); + + // Remove domain names and URLs + $sanitized = preg_replace('/\b[a-z0-9.-]+\.[a-z]{2,}\b/i', '[domain]', $sanitized); + + // Remove UUIDs and other identifiers + $sanitized = preg_replace('/\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/i', '[ID]', $sanitized); + + // Convert technical terms to user-friendly language + $sanitized = str_replace([ + 'is not a valid email address', + 'does not exist', + 'Field required', + 'value_error', + 'missing', + 'type_error', + ], [ + 'is not valid', + 'is not available', + 'is required', + 'is invalid', + 'is missing', + 'is not correct', + ], $sanitized); + + // Add field context + return "The {$field} {$sanitized}"; + } }