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()) {
|
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}";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user