Feat: Implement Stripe payment initiation and webhook handling (Step 11)
This commit is contained in:
parent
04bf4adfb0
commit
932e7aaed5
6 changed files with 482 additions and 128 deletions
|
|
@ -11,7 +11,9 @@
|
|||
],
|
||||
"require": {
|
||||
"php": ">=7.4",
|
||||
"phpmailer/phpmailer": "^6.8"
|
||||
"phpmailer/phpmailer": "^6.8",
|
||||
"stripe/stripe-php": "^10.0 || ^11.0 || ^12.0 || ^13.0 || ^14.0",
|
||||
"symfony/polyfill-mbstring": "^1.27"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
|
|
|
|||
141
composer.lock
generated
141
composer.lock
generated
|
|
@ -4,7 +4,7 @@
|
|||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "aa1767c4b73d4c64f1f4b5531b361ba5",
|
||||
"content-hash": "a2f0f2a9d31ce45269c808c0c1b65daf",
|
||||
"packages": [
|
||||
{
|
||||
"name": "phpmailer/phpmailer",
|
||||
|
|
@ -86,6 +86,145 @@
|
|||
}
|
||||
],
|
||||
"time": "2024-11-24T18:04:13+00:00"
|
||||
},
|
||||
{
|
||||
"name": "stripe/stripe-php",
|
||||
"version": "v14.10.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/stripe/stripe-php.git",
|
||||
"reference": "7e1c4b5d2beadeaeddc42fd1f8a50fdb18b37f30"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/stripe/stripe-php/zipball/7e1c4b5d2beadeaeddc42fd1f8a50fdb18b37f30",
|
||||
"reference": "7e1c4b5d2beadeaeddc42fd1f8a50fdb18b37f30",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-curl": "*",
|
||||
"ext-json": "*",
|
||||
"ext-mbstring": "*",
|
||||
"php": ">=5.6.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"friendsofphp/php-cs-fixer": "3.5.0",
|
||||
"phpstan/phpstan": "^1.2",
|
||||
"phpunit/phpunit": "^5.7 || ^9.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "2.0-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Stripe\\": "lib/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Stripe and contributors",
|
||||
"homepage": "https://github.com/stripe/stripe-php/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Stripe PHP Library",
|
||||
"homepage": "https://stripe.com/",
|
||||
"keywords": [
|
||||
"api",
|
||||
"payment processing",
|
||||
"stripe"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/stripe/stripe-php/issues",
|
||||
"source": "https://github.com/stripe/stripe-php/tree/v14.10.0"
|
||||
},
|
||||
"time": "2024-06-13T21:04:47+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-mbstring",
|
||||
"version": "v1.31.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-mbstring.git",
|
||||
"reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/85181ba99b2345b0ef10ce42ecac37612d9fd341",
|
||||
"reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.2"
|
||||
},
|
||||
"provide": {
|
||||
"ext-mbstring": "*"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-mbstring": "For best performance"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"thanks": {
|
||||
"url": "https://github.com/symfony/polyfill",
|
||||
"name": "symfony/polyfill"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"files": [
|
||||
"bootstrap.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"Symfony\\Polyfill\\Mbstring\\": ""
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Nicolas Grekas",
|
||||
"email": "p@tchwork.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Symfony polyfill for the Mbstring extension",
|
||||
"homepage": "https://symfony.com",
|
||||
"keywords": [
|
||||
"compatibility",
|
||||
"mbstring",
|
||||
"polyfill",
|
||||
"portable",
|
||||
"shim"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.31.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-09-09T11:45:10+00:00"
|
||||
}
|
||||
],
|
||||
"packages-dev": [
|
||||
|
|
|
|||
|
|
@ -293,3 +293,17 @@ function quiztech_configure_smtp( $phpmailer ) {
|
|||
}
|
||||
add_action( 'phpmailer_init', 'quiztech_configure_smtp' );
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Register REST API endpoints.
|
||||
*/
|
||||
function quiztech_register_rest_routes() {
|
||||
register_rest_route( 'quiztech/v1', '/webhook/stripe', array(
|
||||
'methods' => 'POST',
|
||||
'callback' => '\Quiztech\AssessmentPlatform\Includes\quiztech_handle_payment_webhook',
|
||||
'permission_callback' => '__return_true' // Allow public access - Stripe needs to reach this
|
||||
) );
|
||||
}
|
||||
add_action( 'rest_api_init', 'quiztech_register_rest_routes' );
|
||||
|
||||
|
|
|
|||
|
|
@ -96,6 +96,21 @@ class SettingsPage {
|
|||
]
|
||||
);
|
||||
|
||||
// Stripe Webhook Secret Field
|
||||
add_settings_field(
|
||||
'quiztech_stripe_webhook_secret', // Field ID
|
||||
__( 'Stripe Webhook Secret', 'quiztech' ), // Field Title
|
||||
[ $this, 'render_text_field' ], // Field render callback
|
||||
$this->page_slug, // Page slug
|
||||
'quiztech_stripe_section', // Section ID
|
||||
[ // Arguments for callback
|
||||
'id' => 'quiztech_stripe_webhook_secret',
|
||||
'option_name' => $this->option_name,
|
||||
'key' => 'stripe_webhook_secret',
|
||||
'type' => 'password', // Mask the input
|
||||
'description' => __( 'Enter your Stripe webhook signing secret (whsec_...). Required for processing payments.', 'quiztech' )
|
||||
]
|
||||
);
|
||||
|
||||
// --- SMTP Section ---
|
||||
add_settings_section(
|
||||
|
|
@ -393,6 +408,11 @@ class SettingsPage {
|
|||
|
||||
/**
|
||||
* Sanitizes the settings array before saving.
|
||||
if ( isset( $input['stripe_webhook_secret'] ) ) {
|
||||
// Basic sanitization, starts with whsec_
|
||||
$sanitized_input['stripe_webhook_secret'] = sanitize_text_field( $input['stripe_webhook_secret'] );
|
||||
}
|
||||
|
||||
*
|
||||
* @param array $input The raw input array from the form.
|
||||
* @return array The sanitized array.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,12 @@
|
|||
<?php
|
||||
namespace Quiztech\AssessmentPlatform\Gateways;
|
||||
|
||||
use Stripe\Stripe;
|
||||
use Stripe\Checkout\Session;
|
||||
use Stripe\Exception\ApiErrorException;
|
||||
use Stripe\Webhook;
|
||||
use WP_Error;
|
||||
|
||||
/**
|
||||
* Handles Stripe payment gateway interactions.
|
||||
*/
|
||||
|
|
@ -12,69 +18,129 @@ class StripeGateway {
|
|||
* @param int $user_id The ID of the user making the purchase.
|
||||
* @param string $item_id Identifier for the credit pack or item.
|
||||
* @param int $quantity Quantity being purchased.
|
||||
* @param array $metadata Additional data to pass to Stripe.
|
||||
* @return mixed Stripe Session ID, redirect URL, or WP_Error on failure.
|
||||
* @param int $price_in_cents Price for one unit in the smallest currency unit (e.g., cents).
|
||||
* @param string $currency Currency code (e.g., 'USD').
|
||||
* @param array $metadata Additional data to pass to Stripe and retrieve via webhook.
|
||||
* @return WP_Error|void Returns WP_Error on failure. Redirects user on success.
|
||||
*/
|
||||
public function initiatePayment( $user_id, $item_id, $quantity = 1, $metadata = [] ) {
|
||||
// Placeholder for Stripe payment initiation logic
|
||||
// 1. Get item details (price, name) based on $item_id
|
||||
// 2. Format line items for Stripe Checkout
|
||||
// 3. Set success/cancel URLs
|
||||
// 4. Call Stripe API to create a Checkout Session
|
||||
// 5. Return session ID or redirect URL
|
||||
public function initiatePayment( $user_id, $item_id, $quantity, $price_in_cents, $currency, $metadata ) {
|
||||
// Retrieve Stripe Secret Key
|
||||
$options = get_option('quiztech_settings');
|
||||
$secret_key = isset($options['stripe_secret_key']) ? trim($options['stripe_secret_key']) : '';
|
||||
|
||||
\error_log('Stripe Payment Initiation Called - Placeholder');
|
||||
// Example error return:
|
||||
// return new \WP_Error('stripe_error', 'Failed to create Stripe session.');
|
||||
return false; // Placeholder
|
||||
if ( empty( $secret_key ) ) {
|
||||
error_log( 'Quiztech Stripe Error: Secret key is not configured.' );
|
||||
return new WP_Error( 'stripe_config_error', __( 'Stripe payment gateway is not configured correctly. Please contact the site administrator.', 'quiztech' ) );
|
||||
}
|
||||
|
||||
// Ensure Stripe library is loaded (via Composer autoload)
|
||||
if ( ! class_exists( '\Stripe\Stripe' ) ) { // Check with leading slash here is okay as it's a general check
|
||||
error_log( 'Quiztech Stripe Error: Stripe PHP library not found. Ensure composer install was run.' );
|
||||
return new WP_Error( 'stripe_lib_error', __( 'Payment processing library is missing. Please contact the site administrator.', 'quiztech' ) );
|
||||
}
|
||||
|
||||
try {
|
||||
Stripe::setApiKey( $secret_key );
|
||||
|
||||
// Get the URL for the Manage Credits page
|
||||
$manage_credits_page = get_page_by_path('manage-credits'); // Assumes page slug is 'manage-credits'
|
||||
if (!$manage_credits_page) {
|
||||
return new WP_Error('page_not_found', __('Manage Credits page not found.', 'quiztech'));
|
||||
}
|
||||
$base_url = get_permalink($manage_credits_page->ID);
|
||||
|
||||
$success_url = add_query_arg( 'purchase_status', 'success', $base_url );
|
||||
$cancel_url = add_query_arg( 'purchase_status', 'cancelled', $base_url );
|
||||
|
||||
// Create a descriptive name for the line item
|
||||
// Extract number from item_id like '10_credits'
|
||||
preg_match('/^(\d+)_credits$/', $item_id, $matches);
|
||||
$credit_amount_display = isset($matches[1]) ? $matches[1] : $item_id;
|
||||
$item_name = sprintf( __( 'Quiztech Credits - %s Pack', 'quiztech' ), $credit_amount_display );
|
||||
|
||||
|
||||
$session = Session::create( [
|
||||
'payment_method_types' => ['card'],
|
||||
'line_items' => [
|
||||
[
|
||||
'price_data' => [
|
||||
'currency' => strtolower($currency), // Stripe expects lowercase currency
|
||||
'product_data' => [
|
||||
'name' => $item_name,
|
||||
// 'description' => 'Optional description here',
|
||||
],
|
||||
'unit_amount' => $price_in_cents,
|
||||
],
|
||||
'quantity' => $quantity,
|
||||
],
|
||||
],
|
||||
'mode' => 'payment',
|
||||
'success_url' => $success_url,
|
||||
'cancel_url' => $cancel_url,
|
||||
'client_reference_id' => $user_id, // Pass user ID for reference
|
||||
'metadata' => $metadata, // Pass our custom metadata
|
||||
] );
|
||||
|
||||
// Redirect to Stripe Checkout
|
||||
if ( isset( $session->url ) ) {
|
||||
wp_safe_redirect( $session->url );
|
||||
exit;
|
||||
} else {
|
||||
return new WP_Error( 'stripe_session_error', __( 'Could not create Stripe Checkout session.', 'quiztech' ) );
|
||||
}
|
||||
|
||||
} catch ( ApiErrorException $e ) {
|
||||
error_log( 'Stripe API Error: ' . $e->getMessage() );
|
||||
return new WP_Error( 'stripe_api_error', sprintf( __( 'Payment gateway error: %s', 'quiztech' ), $e->getMessage() ) );
|
||||
} catch ( \Exception $e ) {
|
||||
error_log( 'General Payment Error: ' . $e->getMessage() );
|
||||
return new WP_Error( 'payment_error', __( 'An unexpected error occurred during payment processing.', 'quiztech' ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle incoming Stripe webhooks.
|
||||
* Verify and construct the Stripe Webhook event.
|
||||
*
|
||||
* @param array $payload The webhook payload (usually from file_get_contents('php://input')).
|
||||
* @param string $payload Raw request body.
|
||||
* @param string $signature Stripe-Signature header value.
|
||||
* @param string $webhook_secret The webhook endpoint secret.
|
||||
* @return \Stripe\Event|WP_Error Stripe Event object on success, WP_Error on failure.
|
||||
*/
|
||||
public function verifyWebhook( $payload, $signature, $webhook_secret ) {
|
||||
try {
|
||||
// Ensure Stripe library is loaded
|
||||
if ( ! class_exists( '\Stripe\Webhook' ) ) { // Check with leading slash here is okay as it's a general check
|
||||
throw new \Exception('Stripe PHP library Webhook class not found.');
|
||||
}
|
||||
|
||||
$event = Webhook::constructEvent(
|
||||
$payload, $signature, $webhook_secret
|
||||
);
|
||||
return $event;
|
||||
|
||||
} catch( \UnexpectedValueException $e ) {
|
||||
// Invalid payload
|
||||
return new WP_Error('stripe_webhook_payload_error', 'Invalid webhook payload: ' . $e->getMessage());
|
||||
} catch( \Stripe\Exception\SignatureVerificationException $e ) {
|
||||
// Invalid signature
|
||||
return new WP_Error('stripe_webhook_signature_error', 'Invalid webhook signature: ' . $e->getMessage());
|
||||
} catch ( \Exception $e ) {
|
||||
// Other errors
|
||||
return new WP_Error('stripe_webhook_error', 'Webhook processing error: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle incoming Stripe webhooks. (DEPRECATED - Logic moved to quiztech_handle_payment_webhook)
|
||||
*
|
||||
* @param string $payload The webhook payload (usually from file_get_contents('php://input')).
|
||||
* @param string $signature The value of the Stripe-Signature header.
|
||||
* @return bool True if handled successfully, false otherwise.
|
||||
*/
|
||||
public function handleWebhook( $payload, $signature ) {
|
||||
// Placeholder for Stripe webhook handling logic
|
||||
// 1. Verify the webhook signature using the endpoint secret.
|
||||
// 2. Parse the event object from the payload.
|
||||
// 3. Handle specific event types (e.g., 'checkout.session.completed').
|
||||
// 4. If payment successful:
|
||||
// - Extract relevant data (user_id from metadata, transaction_id, credits_purchased).
|
||||
// - Call \Quiztech\AssessmentPlatform\Includes\quiztech_process_successful_payment().
|
||||
// 5. Return appropriate response to Stripe (e.g., 200 OK).
|
||||
|
||||
\error_log('Stripe Webhook Handler Called - Placeholder');
|
||||
|
||||
// Example event handling structure:
|
||||
// $event = null;
|
||||
// try {
|
||||
// $event = \Stripe\Webhook::constructEvent(
|
||||
// $payload, $signature, 'YOUR_STRIPE_WEBHOOK_SECRET'
|
||||
// );
|
||||
// } catch(\UnexpectedValueException $e) {
|
||||
// // Invalid payload
|
||||
// \error_log('Stripe Webhook Error: Invalid payload.');
|
||||
// return false;
|
||||
// } catch(\Stripe\Exception\SignatureVerificationException $e) {
|
||||
// // Invalid signature
|
||||
// \error_log('Stripe Webhook Error: Invalid signature.');
|
||||
// return false;
|
||||
// }
|
||||
|
||||
// // Handle the event
|
||||
// switch ($event->type) {
|
||||
// case 'checkout.session.completed':
|
||||
// $session = $event->data->object;
|
||||
// // Extract data and call quiztech_process_successful_payment...
|
||||
// break;
|
||||
// // ... handle other event types
|
||||
// default:
|
||||
// \error_log('Received unknown Stripe event type ' . $event->type);
|
||||
// }
|
||||
|
||||
return true; // Placeholder
|
||||
// This method is deprecated in favor of verifyWebhook called from the REST handler.
|
||||
// Kept for potential future use or different webhook structures.
|
||||
error_log('StripeGateway::handleWebhook called - This method is deprecated.');
|
||||
return false; // Indicate not handled here
|
||||
}
|
||||
}
|
||||
|
|
@ -25,60 +25,151 @@ if ( ! \defined( 'WPINC' ) ) {
|
|||
* @return mixed Gateway-specific response (e.g., redirect URL, session ID) or WP_Error on failure.
|
||||
*/
|
||||
function quiztech_initiate_credit_purchase( $user_id, $item_id, $quantity = 1 ) {
|
||||
// --- Implementation Required ---
|
||||
// 1. Validate input: Ensure $user_id is valid, $item_id exists, $quantity is positive integer.
|
||||
// 2. Get Item Details: Fetch price and credits amount for $item_id (requires defining how items are stored - e.g., CPT, options).
|
||||
// 3. Get Active Gateway: Read plugin settings (e.g., get_option('quiztech_settings')) to find the configured gateway slug.
|
||||
$active_gateway = 'stripe'; // Placeholder: Replace with dynamic logic reading settings.
|
||||
|
||||
if ( 'stripe' === $active_gateway ) {
|
||||
$gateway = new StripeGateway();
|
||||
// 4. Pass Metadata: Gather necessary data (user_id, item_id, quantity, credits_amount, price)
|
||||
// and pass it appropriately to the gateway's initiatePayment method, likely for Stripe metadata.
|
||||
$result = $gateway->initiatePayment( $user_id, $item_id, $quantity /*, $metadata_array */ ); // Example modification
|
||||
return $result; // Return whatever the gateway method returns (e.g., session ID, URL, WP_Error)
|
||||
// 1. Validate input
|
||||
if ( ! $user_id || ! \get_user_by( 'ID', $user_id ) ) {
|
||||
return new \WP_Error( 'invalid_user', \__( 'Invalid user ID provided.', 'quiztech' ) );
|
||||
}
|
||||
if ( empty( $item_id ) ) {
|
||||
return new \WP_Error( 'invalid_item', \__( 'No credit package specified.', 'quiztech' ) );
|
||||
}
|
||||
$quantity = absint( $quantity );
|
||||
if ( $quantity <= 0 ) {
|
||||
return new \WP_Error( 'invalid_quantity', \__( 'Quantity must be at least 1.', 'quiztech' ) );
|
||||
}
|
||||
|
||||
// If no active or supported gateway found
|
||||
return new \WP_Error( 'no_gateway', \__( 'No active or supported payment gateway configured.', 'quiztech' ) );
|
||||
// 2. Get Item Details (Using hardcoded packages for now, mirroring the theme)
|
||||
// TODO: Move credit packages to a filterable array or WP options for better management.
|
||||
$credit_packages = array(
|
||||
'10_credits' => array( 'amount' => 10, 'price' => '10.00', 'currency' => 'USD' ),
|
||||
'50_credits' => array( 'amount' => 50, 'price' => '45.00', 'currency' => 'USD' ),
|
||||
'100_credits' => array( 'amount' => 100, 'price' => '80.00', 'currency' => 'USD' ),
|
||||
);
|
||||
|
||||
if ( ! isset( $credit_packages[ $item_id ] ) ) {
|
||||
return new \WP_Error( 'invalid_package', \__( 'Selected credit package does not exist.', 'quiztech' ) );
|
||||
}
|
||||
|
||||
$package = $credit_packages[ $item_id ];
|
||||
$credit_amount = $package['amount'];
|
||||
$price_decimal = $package['price'];
|
||||
$currency = strtoupper( $package['currency'] ); // Ensure currency is uppercase
|
||||
|
||||
// Convert price to cents for Stripe
|
||||
$price_in_cents = (int) ( (float) $price_decimal * 100 );
|
||||
|
||||
// 3. Get Active Gateway
|
||||
// TODO: Read active gateway from plugin settings (e.g., get_option('quiztech_settings')['payment_gateway'])
|
||||
$active_gateway = 'stripe'; // Placeholder
|
||||
|
||||
if ( 'stripe' === $active_gateway ) {
|
||||
// Ensure StripeGateway class exists
|
||||
if ( ! class_exists( '\Quiztech\AssessmentPlatform\Gateways\StripeGateway' ) ) {
|
||||
return new \WP_Error( 'gateway_error', \__( 'Stripe gateway class not found.', 'quiztech' ) );
|
||||
}
|
||||
$gateway = new StripeGateway();
|
||||
|
||||
// 4. Prepare Metadata for Stripe and Webhook
|
||||
$metadata = [
|
||||
'user_id' => $user_id,
|
||||
'item_id' => $item_id,
|
||||
'credit_amount' => $credit_amount,
|
||||
'price_paid' => $price_decimal, // Store original decimal price for reference
|
||||
'currency' => $currency,
|
||||
'quantity' => $quantity,
|
||||
'transaction_type'=> 'credit_purchase', // Identify the transaction type
|
||||
];
|
||||
|
||||
// 5. Call Gateway's initiatePayment method
|
||||
$result = $gateway->initiatePayment( $user_id, $item_id, $quantity, $price_in_cents, $currency, $metadata );
|
||||
|
||||
// The gateway method should handle the redirect on success or return WP_Error
|
||||
return $result;
|
||||
}
|
||||
|
||||
// If no active or supported gateway found
|
||||
return new \WP_Error( 'no_gateway', \__( 'No active or supported payment gateway configured.', 'quiztech' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle incoming payment gateway webhooks.
|
||||
* This needs to be registered as a REST endpoint or admin-ajax handler.
|
||||
* This needs to be registered as a REST endpoint.
|
||||
*
|
||||
* @param WP_REST_Request $request The request object.
|
||||
* @return WP_REST_Response The response object.
|
||||
*/
|
||||
function quiztech_handle_payment_webhook() {
|
||||
// --- Implementation Required ---
|
||||
// Identify the gateway. This typically requires unique endpoints per gateway.
|
||||
// Example: Register '/webhook/stripe' and '/webhook/paypal'. The endpoint itself identifies the gateway.
|
||||
$gateway_slug = 'stripe'; // Placeholder: Determine dynamically based on the requested endpoint.
|
||||
function quiztech_handle_payment_webhook( \WP_REST_Request $request ) {
|
||||
// We only handle Stripe for now, identified by the endpoint registration.
|
||||
$gateway_slug = 'stripe';
|
||||
|
||||
if ( 'stripe' === $gateway_slug ) {
|
||||
$payload = @file_get_contents('php://input');
|
||||
$signature = isset($_SERVER['HTTP_STRIPE_SIGNATURE']) ? $_SERVER['HTTP_STRIPE_SIGNATURE'] : '';
|
||||
$payload = $request->get_body();
|
||||
$signature = $request->get_header( 'stripe_signature' ); // Stripe sends 'Stripe-Signature'
|
||||
|
||||
if ( empty($payload) || empty($signature) ) {
|
||||
\wp_send_json_error( 'Missing payload or signature for Stripe webhook.', 400 );
|
||||
return; // Exit
|
||||
}
|
||||
if ( empty( $payload ) || empty( $signature ) ) {
|
||||
\error_log( 'Quiztech Stripe Webhook Error: Missing payload or signature.' );
|
||||
return new \WP_REST_Response( [ 'error' => 'Missing payload or signature.' ], 400 );
|
||||
}
|
||||
|
||||
$gateway = new StripeGateway();
|
||||
$handled = $gateway->handleWebhook( $payload, $signature );
|
||||
// Retrieve the webhook secret from settings
|
||||
$options = get_option( 'quiztech_settings' );
|
||||
$webhook_secret = isset( $options['stripe_webhook_secret'] ) ? trim( $options['stripe_webhook_secret'] ) : '';
|
||||
|
||||
if ( $handled ) {
|
||||
// The handleWebhook method should call quiztech_process_successful_payment internally if needed.
|
||||
\wp_send_json_success( 'Webhook processed.', 200 );
|
||||
} else {
|
||||
\wp_send_json_error( 'Stripe webhook verification or processing failed.', 400 );
|
||||
}
|
||||
return; // Exit
|
||||
}
|
||||
if ( empty( $webhook_secret ) ) {
|
||||
\error_log( 'Quiztech Stripe Webhook Error: Webhook secret is not configured in settings.' );
|
||||
// Return 500 because the server is misconfigured
|
||||
return new \WP_REST_Response( [ 'error' => 'Webhook processing is not configured.' ], 500 );
|
||||
}
|
||||
|
||||
// Handle other gateways or errors if no matching gateway found
|
||||
\wp_send_json_error( 'Invalid or unsupported webhook request.', 400 );
|
||||
// Ensure StripeGateway class exists
|
||||
if ( ! class_exists( '\Quiztech\AssessmentPlatform\Gateways\StripeGateway' ) ) {
|
||||
\error_log( 'Quiztech Stripe Webhook Error: StripeGateway class not found.' );
|
||||
return new \WP_REST_Response( [ 'error' => 'Internal server error.' ], 500 );
|
||||
}
|
||||
|
||||
$gateway = new StripeGateway();
|
||||
$event = $gateway->verifyWebhook( $payload, $signature, $webhook_secret );
|
||||
|
||||
if ( is_wp_error( $event ) ) {
|
||||
\error_log( 'Quiztech Stripe Webhook Error: Verification failed - ' . $event->get_error_message() );
|
||||
return new \WP_REST_Response( [ 'error' => $event->get_error_message() ], 400 );
|
||||
}
|
||||
|
||||
// Handle the event type
|
||||
if ( 'checkout.session.completed' === $event->type ) {
|
||||
$session = $event->data->object;
|
||||
|
||||
// Check payment status
|
||||
if ( $session->payment_status === 'paid' ) {
|
||||
// Extract metadata
|
||||
$metadata = $session->metadata;
|
||||
$user_id = isset( $metadata->user_id ) ? absint( $metadata->user_id ) : 0;
|
||||
$credits_purchased = isset( $metadata->credit_amount ) ? absint( $metadata->credit_amount ) : 0;
|
||||
$transaction_id = $session->payment_intent; // Use payment intent ID as unique transaction ID
|
||||
|
||||
if ( $user_id && $credits_purchased && $transaction_id ) {
|
||||
$processed = quiztech_process_successful_payment( $user_id, $credits_purchased, $transaction_id, $gateway_slug );
|
||||
if ( $processed ) {
|
||||
return new \WP_REST_Response( [ 'status' => 'success' ], 200 );
|
||||
} else {
|
||||
// Error logged within quiztech_process_successful_payment
|
||||
return new \WP_REST_Response( [ 'error' => 'Failed to process payment internally.' ], 500 );
|
||||
}
|
||||
} else {
|
||||
\error_log( 'Quiztech Stripe Webhook Error: Missing required metadata (user_id, credit_amount) or transaction ID in checkout.session.completed event.' );
|
||||
return new \WP_REST_Response( [ 'error' => 'Missing required data in webhook.' ], 400 );
|
||||
}
|
||||
} else {
|
||||
// Log other payment statuses if needed (e.g., 'unpaid')
|
||||
\error_log( 'Quiztech Stripe Webhook Info: Received checkout.session.completed event with status: ' . $session->payment_status );
|
||||
return new \WP_REST_Response( [ 'status' => 'ignored_payment_status' ], 200 );
|
||||
}
|
||||
} else {
|
||||
// Optionally handle other event types like 'payment_intent.succeeded' if needed
|
||||
\error_log( 'Quiztech Stripe Webhook Info: Received unhandled event type: ' . $event->type );
|
||||
return new \WP_REST_Response( [ 'status' => 'unhandled_event_type' ], 200 );
|
||||
}
|
||||
}
|
||||
|
||||
// --- Implementation Required ---
|
||||
// --- Implementation Required --- (Removed as REST registration is now in main plugin file)
|
||||
// Register the webhook handler(s) using WordPress REST API (recommended for webhooks).
|
||||
// Example (needs proper implementation, likely in a dedicated class or main plugin file):
|
||||
// add_action( 'rest_api_init', function () {
|
||||
|
|
@ -100,35 +191,57 @@ function quiztech_handle_payment_webhook() {
|
|||
* @param string $gateway_slug Slug of the gateway (e.g., 'stripe').
|
||||
* @return bool True on success, false on failure.
|
||||
*/
|
||||
function quiztech_process_successful_payment( $user_id, $credits_purchased, $transaction_id, $gateway_slug ) {
|
||||
if ( ! $user_id || ! $credits_purchased > 0 || empty( $transaction_id ) || empty( $gateway_slug ) ) {
|
||||
// Log error: Invalid data received for payment processing.
|
||||
return false;
|
||||
}
|
||||
|
||||
// --- Implementation Required ---
|
||||
// Add checks to prevent processing the same transaction ID multiple times.
|
||||
// This typically involves:
|
||||
// 1. Storing processed transaction IDs (e.g., in post meta on the user_evaluation, a dedicated log table, or user meta).
|
||||
// 2. Querying this storage before updating the balance. If the ID exists, return true (or log) without updating again.
|
||||
// 3. Saving the transaction ID after successfully updating the balance.
|
||||
|
||||
// Update the user's credit balance
|
||||
$result = quiztech_update_user_credit_balance(
|
||||
$user_id,
|
||||
(int) $credits_purchased,
|
||||
\sprintf( '%s_purchase_%s', $gateway_slug, $transaction_id )
|
||||
);
|
||||
|
||||
if ( false !== $result ) {
|
||||
// Optional: Log successful transaction details somewhere (custom table?)
|
||||
// Optional: Send purchase confirmation email to user
|
||||
// do_action('quiztech_credits_purchased', $user_id, $credits_purchased, $transaction_id, $gateway_slug);
|
||||
return true;
|
||||
} else {
|
||||
// Log error: Failed to update credit balance for user $user_id after successful payment $transaction_id.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
||||
function quiztech_process_successful_payment( $user_id, $credits_purchased, $transaction_id, $gateway_slug ) {
|
||||
// 1. Validate Input
|
||||
if ( ! $user_id || ! is_numeric($user_id) || $user_id <= 0 ) {
|
||||
error_log("Quiztech Error: Invalid user ID ({$user_id}) in quiztech_process_successful_payment.");
|
||||
return false;
|
||||
}
|
||||
if ( ! $credits_purchased || ! is_numeric($credits_purchased) || $credits_purchased <= 0 ) {
|
||||
error_log("Quiztech Error: Invalid credits purchased amount ({$credits_purchased}) for user {$user_id} in quiztech_process_successful_payment.");
|
||||
return false;
|
||||
}
|
||||
if ( empty( $transaction_id ) ) {
|
||||
error_log("Quiztech Error: Empty transaction ID for user {$user_id} in quiztech_process_successful_payment.");
|
||||
return false;
|
||||
}
|
||||
if ( empty( $gateway_slug ) ) {
|
||||
error_log("Quiztech Error: Empty gateway slug for user {$user_id}, transaction {$transaction_id} in quiztech_process_successful_payment.");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2. Check for Duplicate Processing (using transients, valid for 1 hour)
|
||||
// A more robust solution might involve a dedicated log table or checking payment intent status directly.
|
||||
$transient_key = 'quiztech_txn_' . md5( $gateway_slug . '_' . $transaction_id );
|
||||
if ( get_transient( $transient_key ) ) {
|
||||
// Already processed recently
|
||||
error_log( "Quiztech Info: Attempted to re-process transaction ID {$transaction_id} for user {$user_id}. Already handled." );
|
||||
return true; // Return true to indicate it's "handled" (already done)
|
||||
}
|
||||
|
||||
// 3. Update the user's credit balance
|
||||
$result = quiztech_update_user_credit_balance(
|
||||
$user_id,
|
||||
(int) $credits_purchased,
|
||||
sprintf( '%s_purchase_%s', $gateway_slug, $transaction_id ) // Use global namespace sprintf
|
||||
);
|
||||
|
||||
if ( false !== $result ) {
|
||||
// 4. Set transient to prevent reprocessing on success
|
||||
set_transient( $transient_key, true, HOUR_IN_SECONDS ); // Store for 1 hour
|
||||
|
||||
// Optional: Log successful transaction details somewhere (custom table?)
|
||||
error_log("Quiztech Info: Successfully processed payment {$transaction_id} for user {$user_id}. Credits added: {$credits_purchased}. New balance: {$result}.");
|
||||
|
||||
// Optional: Send purchase confirmation email to user
|
||||
// do_action('quiztech_credits_purchased', $user_id, $credits_purchased, $transaction_id, $gateway_slug);
|
||||
|
||||
return true;
|
||||
} else {
|
||||
// Log error: Failed to update credit balance for user $user_id after successful payment $transaction_id.
|
||||
error_log("Quiztech Error: Failed to update credit balance for user {$user_id} after successful payment {$transaction_id}.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
||||
Loading…
Add table
Reference in a new issue