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:
@@ -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}";
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user