I have a website offering subscriptions to use my tools. I am having an issue where my script isn't updating users subscriptions start/end dates.
For example, a users registers for a day subscription (day sub for testing, this will be removed for monthly sub)
First day of buying the sub:Subscription Start: 2024-09-06 16:36:14 (Y:M:D)Subscription End: 2024-09-07 16:36:14 (Y:M:D)Next Billing Date: 2024-09-07 06:00:00 (Y:M:D)
Next day when the sub runs out and re-news:Subscription Start: 2024-09-07 06:00:00 (Y:M:D)Subscription End: 2024-09-08 15:20:42 (Y:M:D)Next Billing Date: 2024-09-09 06:00:00 (Y:M:D)
as you can see the sub run out before the "Next Billing Date", i'm really not sure how to fix this issue or i'm doing this in the totally wrong way, any help is much appreciated
sub_update.php - runs every hour on cron
<?phprequire '/var/www/html/includes/config.php';require_once '/var/www/html/payment/paypal/PaypalCheckout.class.php';$paypal = new PaypalCheckout();// Enable mock mode for testing (set to true for mock testing)$is_testing = false;try { // Select all active subscriptions $query = "SELECT user_id, paypal_subscr_id, plan_period_end, plan_interval FROM user_subscriptions WHERE status = 'ACTIVE'"; $stmt = $pdo->prepare($query); $stmt->execute(); $subscriptions = $stmt->fetchAll(PDO::FETCH_ASSOC); foreach ($subscriptions as $subscription) { $user_id = $subscription['user_id']; $paypal_subscr_id = $subscription['paypal_subscr_id']; $plan_interval = $subscription['plan_interval']; // 'day' or 'month' // Fetch subscription info from PayPal or use mock data if testing if ($is_testing) { // Simulate a subscription that has expired and needs renewal $now = new DateTime('2024-09-08 15:42:00', new DateTimeZone('Europe/London')); $past_billing_time = (clone $now)->modify('-1 day')->format('Y-m-d H:i:s'); // 24 hours ago // Mock response for expired subscription $subscription_info = ['status' => 'ACTIVE','next_billing_time' => $past_billing_time, // Past billing time'start_time' => '2024-09-07 15:42:00' // Mock start time 24 hours ago ]; echo "Using mock PayPal data for subscription ID: $paypal_subscr_id\n"; } else { // Real PayPal API call in production $subscription_info = $paypal->getSubscription($paypal_subscr_id); } if ($subscription_info) { // Get the status of the subscription $status = $subscription_info['status']; // Check if the subscription info contains 'next_billing_time' if (!empty($subscription_info['next_billing_time'])) { $next_billing_time = new DateTime($subscription_info['next_billing_time'], new DateTimeZone('UTC')); } else { // Log the missing 'next_billing_time' and skip to the next subscription echo "Warning: 'next_billing_time' not available for subscription ID: $paypal_subscr_id\n"; continue; // Skip this subscription } $now = new DateTime('now', new DateTimeZone('UTC')); if ($next_billing_time < $now) { // Subscription has expired, simulate renewal echo "Subscription expired. Processing renewal...\n"; // Simulate renewal for the next billing period $new_start_time = (clone $now)->format('Y-m-d H:i:s'); if ($plan_interval === 'day') { $new_end_time = (clone $now)->modify('+1 day')->format('Y-m-d H:i:s'); } elseif ($plan_interval === 'month') { $new_end_time = (clone $now)->modify('+1 month')->format('Y-m-d H:i:s'); } // Set next billing date to match subscription end time $new_next_billing_time = $new_end_time; // Update the subscription details in the database $update_sql = "UPDATE user_subscriptions SET plan_period_start = ?, plan_period_end = ?, modified = NOW() WHERE paypal_subscr_id = ?"; $update_stmt = $pdo->prepare($update_sql); $update_stmt->execute([$new_start_time, $new_end_time, $paypal_subscr_id]); // Output user_id, new start time, new end time, and new next billing date echo "Subscription renewed for user ID: $user_id\n"; echo "New Start Time: $new_start_time\n"; echo "New End Time: $new_end_time\n"; echo "New Next Billing Date: $new_next_billing_time\n"; } else { echo "Subscription is still active. No renewal needed.\n"; } // Update the user's premium status based on the subscription status $is_premium = ($status === 'ACTIVE') ? 1 : 0; $update_user_sql = "UPDATE users SET is_premium = ? WHERE id = ?"; $update_user_stmt = $pdo->prepare($update_user_sql); $update_user_stmt->execute([$is_premium, $user_id]); } else { echo "Error: Could not retrieve subscription details for ID: $paypal_subscr_id\n"; } } echo "Subscription updates complete.";} catch (Exception $e) { error_log("Error updating subscriptions: " . $e->getMessage());}?>
paypal_checkout_init.php:
<?phpsession_start();ini_set('display_errors', 1);ini_set('display_startup_errors', 1);error_reporting(E_ALL);require '/var/www/html/includes/config.php';require_once 'PaypalCheckout.class.php';$paypal = new PaypalCheckout();$response = array('status' => 0, 'msg' => 'Request Failed!');try { if (!empty($_POST['request_type']) && $_POST['request_type'] == 'create_plan') { $plan_id = $_POST['plan_id']; // Fetch plan details from the database $sqlQ = "SELECT name, price, `interval`, interval_count FROM plans WHERE id=?"; $stmt = $pdo->prepare($sqlQ); $stmt->execute([$plan_id]); $plan = $stmt->fetch(PDO::FETCH_ASSOC); if (!$plan) { throw new Exception('Plan not found.'); } $plan_data = array('name' => $plan['name'],'price' => $plan['price'],'interval' => $plan['interval'],'interval_count' => $plan['interval_count'], ); $subscr_plan = $paypal->createPlan($plan_data); if (!empty($subscr_plan)) { $response = array('status' => 1,'data' => $subscr_plan ); } else { throw new Exception('Failed to create plan.'); } } elseif (!empty($_POST['request_type']) && $_POST['request_type'] == 'capture_subscr') { $order_id = $_POST['order_id']; $subscription_id = $_POST['subscription_id']; $db_plan_id = $_POST['plan_id']; // Fetch & validate subscription with PayPal API $subscr_data = $paypal->getSubscription($subscription_id); if (!empty($subscr_data)) { $status = $subscr_data['status']; $subscr_id = $subscr_data['id']; $plan_id = $subscr_data['plan_id']; $custom_user_id = $_SESSION['user_id']; $start_time = new DateTime($subscr_data['start_time']); $valid_from = $start_time->format("Y-m-d H:i:s"); // Retrieve plan details again to ensure the interval details are available $sqlQ = "SELECT `interval`, interval_count FROM plans WHERE id=?"; $stmt = $pdo->prepare($sqlQ); $stmt->execute([$db_plan_id]); $plan = $stmt->fetch(PDO::FETCH_ASSOC); if (!$plan) { throw new Exception('Plan details not found during subscription capture.'); } $interval_unit = strtoupper($plan['interval']); // 'month' $interval_count = $plan['interval_count']; // 1 $valid_to = $start_time->modify("+$interval_count $interval_unit")->format("Y-m-d H:i:s"); $last_payment_amount = $subscr_data['billing_info']['last_payment']['amount']['value'] ?? null; $last_payment_currency = $subscr_data['billing_info']['last_payment']['amount']['currency_code'] ?? null; $transaction_id = $subscr_data['id']; $subscriber = $subscr_data['subscriber']; $subscriber_email = $subscriber['email_address']; $subscriber_id = $subscriber['payer_id']; $subscriber_name = trim($subscriber['name']['given_name'] . '' . $subscriber['name']['surname']); if (!empty($subscr_id) && $status == 'ACTIVE') { $sqlQ = "SELECT id FROM user_subscriptions WHERE paypal_order_id = ?"; $stmt = $pdo->prepare($sqlQ); $stmt->execute([$order_id]); $row_id = $stmt->fetchColumn(); if (!empty($row_id)) { $user_subscription_id = $row_id; } else { $sqlQ = "INSERT INTO user_subscriptions ( user_id, plan_id, paypal_order_id, paypal_plan_id, paypal_subscr_id, plan_period_start, plan_period_end, paid_amount, paid_amount_currency, plan_interval, plan_interval_count, subscriber_id, customer_name, customer_email, status, created, modified ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW())"; $stmt = $pdo->prepare($sqlQ); $insert = $stmt->execute([ $custom_user_id, $db_plan_id, $order_id, $plan_id, $subscr_id, $valid_from, $valid_to, $last_payment_amount, $last_payment_currency, $plan['interval'], $plan['interval_count'], $subscriber_id, $subscriber_name, $subscriber_email, $status ]); if ($insert) { $user_subscription_id = $pdo->lastInsertId(); // Update subscription ID in users table $sqlQ = "UPDATE users SET subscription_id = ? WHERE id = ?"; $stmt = $pdo->prepare($sqlQ); $stmt->execute([$user_subscription_id, $custom_user_id]); } } // Insert the payment record into the subscription_payments table if (!empty($user_subscription_id)) { $sqlQ = "INSERT INTO subscription_payments ( user_subscription_id, payment_date, paid_amount, paid_amount_currency, transaction_id, payment_status, created_at ) VALUES (?, NOW(), ?, ?, ?, ?, NOW())"; $stmt = $pdo->prepare($sqlQ); $stmt->execute([ $user_subscription_id, $last_payment_amount, $last_payment_currency, $transaction_id, $status ]); } if (!empty($user_subscription_id)) { $ref_id_enc = base64_encode($order_id); $response = array('status' => 1, 'msg' => 'Subscription created!', 'ref_id' => $ref_id_enc); } } } else { throw new Exception('Subscription data is empty.'); } }} catch (Exception $e) { $response['msg'] = $e->getMessage();}echo json_encode($response);?>
PaypalCheckout.class.php:
<?phpclass PaypalCheckout { public $paypalAuthAPI = PAYPAL_SANDBOX ? ''; public $paypalProductAPI = PAYPAL_SANDBOX ? ''; public $paypalBillingAPI = PAYPAL_SANDBOX ? ''; public $paypalClientID = PAYPAL_SANDBOX ? PAYPAL_SANDBOX_CLIENT_ID : PAYPAL_PROD_CLIENT_ID; private $paypalSecret = PAYPAL_SANDBOX ? PAYPAL_SANDBOX_CLIENT_SECRET : PAYPAL_PROD_CLIENT_SECRET; // Function to cancel a subscription public function cancelSubscription($subscription_id) { $accessToken = $this->generateAccessToken(); if (empty($accessToken)) { return false; } $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $this->paypalBillingAPI . '/subscriptions/' . $subscription_id . '/cancel'); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE); curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-Type: application/json', 'Authorization: Bearer '. $accessToken)); curl_setopt($ch, CURLOPT_POST, true); $api_data = json_decode(curl_exec($ch), true); $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($http_code != 204) { // Log the failure error_log("Failed to cancel subscription ID $subscription_id. Response: " . print_r($api_data, true)); return false; } return true; } // Function to generate an access token public function generateAccessToken() { $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $this->paypalAuthAPI); curl_setopt($ch, CURLOPT_HEADER, false); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_USERPWD, $this->paypalClientID . ":" . $this->paypalSecret); curl_setopt($ch, CURLOPT_POSTFIELDS, "grant_type=client_credentials"); $auth_response = json_decode(curl_exec($ch)); $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($http_code != 200 && !empty($auth_response->error)) { throw new Exception('Failed to generate Access Token: ' . $auth_response->error . '>>> ' . $auth_response->error_description); } return $auth_response->access_token ?? false; } // Function to create a billing plan public function createPlan($planInfo) { $accessToken = $this->generateAccessToken(); if (empty($accessToken)) { return false; } // Create the product first $postParams = array("name" => $planInfo['name'],"type" => "DIGITAL","category" => "SOFTWARE" ); $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $this->paypalProductAPI); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE); curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-Type: application/json', 'Authorization: Bearer '. $accessToken)); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($postParams)); $api_resp = curl_exec($ch); $pro_api_data = json_decode($api_resp); $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($http_code != 200 && $http_code != 201) { throw new Exception('Failed to create Product (' . $http_code . '): ' . $api_resp); } if (!empty($pro_api_data->id)) { $postParams = array("product_id" => $pro_api_data->id,"name" => $planInfo['name'],"billing_cycles" => array( array("frequency" => array("interval_unit" => $planInfo['interval'], // 'DAY' or 'MONTH'"interval_count" => $planInfo['interval_count'] // 1 for both cases ),"tenure_type" => "REGULAR","sequence" => 1,"total_cycles" => 999,"pricing_scheme" => array("fixed_price" => array("value" => $planInfo['price'],"currency_code" => CURRENCY ) ), ) ),"payment_preferences" => array("auto_bill_outstanding" => true ) ); $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $this->paypalBillingAPI.'/plans'); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE); curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-Type: application/json', 'Authorization: Bearer '. $accessToken)); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($postParams)); $api_resp = curl_exec($ch); $plan_api_data = json_decode($api_resp, true); $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($http_code != 200 && $http_code != 201) { throw new Exception('Failed to create Plan (' . $http_code . '): ' . $api_resp); } return !empty($plan_api_data) ? $plan_api_data : false; } else { return false; } } private function logApiResponse($response, $context = '') { file_put_contents('paypal_api.log', date('Y-m-d H:i:s') . " [$context] " . print_r($response, true) . PHP_EOL, FILE_APPEND); } public function getSubscription($subscription_id) { $accessToken = $this->generateAccessToken(); if (empty($accessToken)) { return false; } $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $this->paypalBillingAPI . '/subscriptions/' . $subscription_id); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE); curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-Type: application/json', 'Authorization: Bearer '. $accessToken)); curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET'); $api_data = json_decode(curl_exec($ch), true); $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); $this->logApiResponse($api_data, 'getSubscription'); if ($http_code != 200 && !empty($api_data['error'])) { throw new Exception('Error ' . $api_data['error'] . ': ' . $api_data['error_description']); } return !empty($api_data) && $http_code == 200 ? $api_data : false; } // Function to verify PayPal subscription data (newly added) public function verifySubscriptionData($subscription_id) { $subscription_data = $this->getSubscription($subscription_id); if (!$subscription_data) { throw new Exception("Failed to retrieve subscription data."); } // Optionally, you can log the subscription data for debugging // file_put_contents('subscription_debug.log', print_r($subscription_data, true), FILE_APPEND); // Extract important fields $status = $subscription_data['status'] ?? 'Unknown'; $next_billing_time = $subscription_data['billing_info']['next_billing_time'] ?? 'N/A'; $last_payment_amount = $subscription_data['billing_info']['last_payment']['amount']['value'] ?? 'N/A'; $last_payment_currency = $subscription_data['billing_info']['last_payment']['amount']['currency_code'] ?? 'N/A'; return ['status' => $status,'next_billing_time' => $next_billing_time,'last_payment_amount' => $last_payment_amount,'last_payment_currency' => $last_payment_currency ]; }}
what you expected to happen:
Users buy a subscription and able to view on there account:Subscription Start:Subscription End:Next Billing Date:
and for it to update accordingly based on the plans (monthly)