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
This commit is contained in:
idevakk
2025-12-04 12:30:39 -08:00
parent 8950988eac
commit cd8d6f1165

View File

@@ -220,8 +220,19 @@ class PolarProvider implements PaymentProviderContract
]); ]);
if (! $response->successful()) { if (! $response->successful()) {
$errorMessage = 'Polar checkout creation failed: '.$response->body(); $statusCode = $response->status();
Log::error($errorMessage); $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); throw new \Exception($errorMessage);
} }
@@ -460,8 +471,12 @@ class PolarProvider implements PaymentProviderContract
$response = $this->makeAuthenticatedRequest('GET', '/subscriptions/'.$providerSubscriptionId); $response = $this->makeAuthenticatedRequest('GET', '/subscriptions/'.$providerSubscriptionId);
if (! $response->successful()) { if (! $response->successful()) {
Log::error('Failed to retrieve Polar subscription: '.$response->body()); Log::error('Failed to retrieve Polar subscription', [
throw new \Exception('Polar subscription not found: '.$response->status()); 'status_code' => $response->status(),
'response' => $response->json(),
'subscription_id' => $providerSubscriptionId,
]);
throw new \Exception('Subscription not found. Please check your subscription details.');
} }
$polarSubscription = $response->json(); $polarSubscription = $response->json();
@@ -854,9 +869,13 @@ class PolarProvider implements PaymentProviderContract
} }
} }
$errorMessage = 'Failed to create Polar customer: '.$response->body(); Log::error('Failed to create Polar customer', [
Log::error($errorMessage); 'status_code' => $response->status(),
throw new \Exception($errorMessage); 'response' => $response->json(),
'user_id' => $user->id,
]);
throw new \Exception('Failed to create customer account. Please try again or contact support.');
} }
$customer = $response->json(); $customer = $response->json();
@@ -964,9 +983,13 @@ class PolarProvider implements PaymentProviderContract
$response = $this->makeAuthenticatedRequest('POST', '/products', $productData); $response = $this->makeAuthenticatedRequest('POST', '/products', $productData);
if (! $response->successful()) { if (! $response->successful()) {
$errorMessage = 'Failed to create Polar product: '.$response->body(); Log::error('Failed to create Polar product', [
Log::error($errorMessage); 'status_code' => $response->status(),
throw new \Exception($errorMessage); 'response' => $response->json(),
'plan_id' => $plan->id,
]);
throw new \Exception('Failed to create payment plan. Please try again or contact support.');
} }
$product = $response->json(); $product = $response->json();
@@ -1391,4 +1414,127 @@ class PolarProvider implements PaymentProviderContract
Log::error('Import to Polar payments not implemented'); Log::error('Import to Polar payments not implemented');
throw new \Exception('Import subscription data not implemented for Polar'); 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}";
}
} }