From e63ea73beb85efd738ceb4511e51131bcc273c63 Mon Sep 17 00:00:00 2001 From: Daniel Neto Date: Wed, 23 Jul 2025 10:57:06 -0300 Subject: [PATCH] Add Authorize.Net subscription management functionality - Implemented cancelSubscription.json.php for handling subscription cancellations via JSON API. - Created cancelSubscription.php for managing subscription views and cancellation in the UI. - Developed getAcceptHostedToken.json.php to generate payment tokens for Authorize.Net. - Added getProfileManager.json.php for managing user profiles with Authorize.Net. - Implemented getSubscriptionStatus.json.php to check the status of subscriptions. - Created getSubscriptions.json.php to retrieve all subscriptions for the logged-in user. - Added SQL installation script for webhook logging. - Implemented processPayment.json.php for processing payments through Authorize.Net. - Developed webhook.php to handle incoming webhooks from Authorize.Net. - Integrated YPTWallet with Authorize.Net for wallet funding and subscription management. - Added confirmButton.php and confirmRecurrentButton.php for payment confirmation buttons in YPTWallet. --- plugin/AuthorizeNet/AuthorizeNet.php | 1589 +++++++++++++++++ .../AuthorizeNet/Objects/Anet_webhook_log.php | 156 ++ .../View/Anet_webhook_log/add.json.php | 33 + .../View/Anet_webhook_log/delete.json.php | 20 + .../View/Anet_webhook_log/index.php | 19 + .../View/Anet_webhook_log/index_body.php | 169 ++ .../View/Anet_webhook_log/list.json.php | 16 + plugin/AuthorizeNet/View/editor.php | 29 + plugin/AuthorizeNet/acceptHostedReturn.php | 90 + .../AuthorizeNet/cancelSubscription.json.php | 86 + plugin/AuthorizeNet/cancelSubscription.php | 496 +++++ .../getAcceptHostedToken.json.php | 67 + .../AuthorizeNet/getProfileManager.json.php | 19 + .../getSubscriptionStatus.json.php | 78 + plugin/AuthorizeNet/getSubscriptions.json.php | 72 + plugin/AuthorizeNet/install/install.sql | 22 + plugin/AuthorizeNet/processPayment.json.php | 29 + plugin/AuthorizeNet/webhook.php | 140 ++ .../YPTWalletAuthorizeNet.php | 38 + .../YPTWalletAuthorizeNet/confirmButton.php | 62 + .../confirmRecurrentButton.php | 82 + 21 files changed, 3312 insertions(+) create mode 100644 plugin/AuthorizeNet/AuthorizeNet.php create mode 100644 plugin/AuthorizeNet/Objects/Anet_webhook_log.php create mode 100644 plugin/AuthorizeNet/View/Anet_webhook_log/add.json.php create mode 100644 plugin/AuthorizeNet/View/Anet_webhook_log/delete.json.php create mode 100644 plugin/AuthorizeNet/View/Anet_webhook_log/index.php create mode 100644 plugin/AuthorizeNet/View/Anet_webhook_log/index_body.php create mode 100644 plugin/AuthorizeNet/View/Anet_webhook_log/list.json.php create mode 100644 plugin/AuthorizeNet/View/editor.php create mode 100644 plugin/AuthorizeNet/acceptHostedReturn.php create mode 100644 plugin/AuthorizeNet/cancelSubscription.json.php create mode 100644 plugin/AuthorizeNet/cancelSubscription.php create mode 100644 plugin/AuthorizeNet/getAcceptHostedToken.json.php create mode 100644 plugin/AuthorizeNet/getProfileManager.json.php create mode 100644 plugin/AuthorizeNet/getSubscriptionStatus.json.php create mode 100644 plugin/AuthorizeNet/getSubscriptions.json.php create mode 100644 plugin/AuthorizeNet/install/install.sql create mode 100644 plugin/AuthorizeNet/processPayment.json.php create mode 100644 plugin/AuthorizeNet/webhook.php create mode 100644 plugin/YPTWallet/plugins/YPTWalletAuthorizeNet/YPTWalletAuthorizeNet.php create mode 100644 plugin/YPTWallet/plugins/YPTWalletAuthorizeNet/confirmButton.php create mode 100644 plugin/YPTWallet/plugins/YPTWalletAuthorizeNet/confirmRecurrentButton.php diff --git a/plugin/AuthorizeNet/AuthorizeNet.php b/plugin/AuthorizeNet/AuthorizeNet.php new file mode 100644 index 0000000000..b201d55b22 --- /dev/null +++ b/plugin/AuthorizeNet/AuthorizeNet.php @@ -0,0 +1,1589 @@ + false, 'expected' => '', 'received' => $received]; + } + $keyBin = hex2bin($signatureKeyHex); + $expected = 'sha512=' . hash_hmac('sha512', $rawBody, $keyBin); + return [ + 'valid' => hash_equals($expected, $received), + 'expected' => $expected, + 'received' => $received + ]; + } + + /** + * Parse webhook JSON and extract common fields. + * + * @param string $rawBody + * @param array $headers + * @param array $allowedEvents + * @return array{ + * error:bool, + * msg?:string, + * data:array, + * payload:array, + * eventType:string, + * transactionId:?string, + * amount:float|null, + * currency:?string, + * metadata:array, + * users_id:?int, + * uniq_key:string, + * signatureValid:bool + * } + */ + public static function parseWebhookRequest(string $rawBody, array $headers, array $allowedEvents = ['net.authorize.payment.authcapture.created']): array + { + $cfg = self::getConfig(); + $sig = self::verifySignature($rawBody, $headers, trim($cfg->signatureKey ?? '')); + + $json = json_decode($rawBody, true); + if (!is_array($json)) { + return ['error' => true, 'msg' => 'Invalid JSON', 'signatureValid' => $sig['valid']]; + } + + $eventType = $json['eventType'] ?? ''; + if (!in_array($eventType, $allowedEvents)) { + return [ + 'error' => true, + 'msg' => 'Ignored event type', + 'eventType' => $eventType, + 'signatureValid' => $sig['valid'] + ]; + } + + $payload = $json['payload'] ?? []; + $transactionId = $payload['id'] ?? ($payload['transId'] ?? null); + $amount = isset($payload['amount']) ? (float)$payload['amount'] : (isset($payload['authAmount']) ? (float)$payload['authAmount'] : null); + $currency = $payload['currencyCode'] ?? ($payload['currency'] ?? null); + $metadata = $payload['metadata'] ?? []; + $users_id = isset($metadata['users_id']) ? (int)$metadata['users_id'] : null; + + $uniq_key = sha1($eventType . ($transactionId ?? 'no-txn')); + + return [ + 'error' => false, + 'data' => $json, + 'payload' => $payload, + 'eventType' => $eventType, + 'transactionId' => $transactionId, + 'amount' => $amount, + 'currency' => $currency, + 'metadata' => $metadata, + 'users_id' => $users_id, + 'uniq_key' => $uniq_key, + 'signatureValid' => $sig['valid'] + ]; + } + + /** + * Try to pull users_id from TransactionDetailsType->userFields. + */ + private static function extractUsersIdFromTxnRaw($txnRaw): ?int + { + if (!$txnRaw || !method_exists($txnRaw, 'getUserFields')) { + return null; + } + $ufList = $txnRaw->getUserFields(); + if ($ufList && method_exists($ufList, 'getUserField')) { + foreach ($ufList->getUserField() as $uf) { + if ($uf->getName() === 'users_id') { + return (int)$uf->getValue(); + } + } + } + return null; + } + + /** + * Analyze webhook (payload + raw txn) to decide if it's subscription or single payment. + * + * @param array $payload + * @param mixed $transactionRawObject + * @return array{ + * isASubscription:bool, + * subscriptionId:?string, + * users_id:?int, + * plans_id:?int, + * amount:float|null, + * currency:?string, + * metadata:array, + * isApproved:bool + * } + */ + public static function analyzeTransactionFromWebhook(array $payload, $transactionRawObject = null): array + { + $metadata = $payload['metadata'] ?? []; + $users_id = isset($metadata['users_id']) ? (int)$metadata['users_id'] : null; + $plans_id = isset($metadata['plans_id']) ? (int)$metadata['plans_id'] : null; + $amount = isset($payload['amount']) ? (float)$payload['amount'] : (isset($payload['authAmount']) ? (float)$payload['authAmount'] : null); + $currency = $payload['currencyCode'] ?? ($payload['currency'] ?? null); + + // detect subscription + $subscriptionId = $payload['subscription']['id'] ?? null; + $isASubscription = !empty($subscriptionId); + + // fallback to raw object if needed + if (!$isASubscription && $transactionRawObject && method_exists($transactionRawObject, 'getSubscription')) { + $sub = $transactionRawObject->getSubscription(); + if ($sub && method_exists($sub, 'getId')) { + $subscriptionId = $sub->getId(); + $isASubscription = !empty($subscriptionId); + } + } + + // For subscriptions, try to extract metadata from transaction order description + if ($isASubscription && $transactionRawObject && method_exists($transactionRawObject, 'getOrder')) { + $order = $transactionRawObject->getOrder(); + if ($order && method_exists($order, 'getDescription')) { + $description = $order->getDescription(); + if (!empty($description)) { + $decodedMeta = json_decode($description, true); + if (is_array($decodedMeta)) { + // Merge subscription metadata with payload metadata + $metadata = array_merge($metadata, $decodedMeta); + if (!$users_id && isset($decodedMeta['users_id'])) { + $users_id = (int)$decodedMeta['users_id']; + } + if (!$plans_id && isset($decodedMeta['plans_id'])) { + $plans_id = (int)$decodedMeta['plans_id']; + } + } + } + } + + // Also check invoice number for plans_id + if ($order && method_exists($order, 'getInvoiceNumber') && !$plans_id) { + $invoiceNumber = $order->getInvoiceNumber(); + if (!empty($invoiceNumber) && is_numeric($invoiceNumber)) { + $plans_id = (int)$invoiceNumber; + } + } + } + + // fallback users_id from raw + if (!$users_id && $transactionRawObject) { + $users_id = self::extractUsersIdFromTxnRaw($transactionRawObject); + } + + // approval check + $isApproved = false; + if ($transactionRawObject && method_exists($transactionRawObject, 'getTransactionStatus')) { + $status = strtolower((string)$transactionRawObject->getTransactionStatus()); + $isApproved = in_array($status, ['capturedpendingsettlement', 'settledsuccessfully'], true); + } elseif (isset($payload['responseCode'])) { + $isApproved = ((int)$payload['responseCode'] === 1); + } + + return [ + 'isASubscription' => $isASubscription, + 'subscriptionId' => $subscriptionId, + 'users_id' => $users_id, + 'plans_id' => $plans_id, + 'amount' => $amount, + 'currency' => $currency, + 'metadata' => $metadata, + 'isApproved' => $isApproved + ]; + } + + /** + * Process a single (one-time) payment: credit wallet, persist log, mark processed. + * + * @param int $users_id Internal user ID to credit. + * @param float $amount Amount to credit. + * @param string $uniq_key Unique key built from event + transactionId to avoid duplicates. + * @param string $eventType Webhook event type. + * @param array $payload Raw payload you want to store in the log (optional). + * @param string $description Optional wallet description. + * @return array{error:bool,msg?:string,logId?:int} + */ + public static function processSinglePayment( + int $users_id, + float $amount, + string $uniq_key, + string $eventType, + array $payload = [], + string $description = 'Authorize.Net one-time payment' + ): array { + global $global; + try { + if ($amount <= 0) { + return ['error' => true, 'msg' => 'Invalid amount']; + } + if (empty($users_id)) { + return ['error' => true, 'msg' => 'Missing users_id']; + } + + if (Anet_webhook_log::alreadyProcessed($uniq_key)) { + _error_log("[Authorize.Net] Duplicate processing prevented ($uniq_key)"); + return ['error' => false, 'msg' => 'Already processed']; + } + + $logId = Anet_webhook_log::createIfNotExists($uniq_key, $eventType, $payload, $users_id); + _error_log("[Authorize.Net] Webhook log created id=$logId"); + + $walletPlugin = AVideoPlugin::loadPluginIfEnabled("YPTWallet"); + if (!$walletPlugin) { + return ['error' => true, 'msg' => 'YPTWallet plugin not enabled']; + } + + $walletPlugin->addBalance($users_id, (float)$amount, $description); + + if (!empty($logId)) { + $log = new Anet_webhook_log($logId); + $log->setProcessed(1); + $log->setModified_php_time(time()); + $log->save(); + _error_log("[Authorize.Net] Log marked as processed id=$logId"); + } + + return ['error' => false, 'logId' => (int)$logId]; + } catch (Throwable $e) { + _error_log('[Authorize.Net] Exception in processSinglePayment: ' . $e->getMessage()); + return ['error' => true, 'msg' => $e->getMessage()]; + } + } + + /** + * Process a subscription charge: ensure subscription is active, then credit wallet and log. + * + * @param string $subscriptionId Authorize.Net ARB subscription ID. + * @param int $users_id Internal user ID to credit. + * @param float $amount Amount to credit. + * @param string $uniq_key Unique key for deduplication. + * @param string $eventType Webhook event type. + * @param array $payload Raw payload to store. + * @param string $description Wallet description (default = 'Authorize.Net subscription charge'). + * @return array{error:bool,msg?:string,active?:bool,status?:string,logId?:int} + */ + public static function processSubscriptionCharge( + string $subscriptionId, + int $users_id, + float $amount, + string $uniq_key, + string $eventType, + array $payload = [], + string $description = 'Authorize.Net subscription charge' + ): array { + try { + if (empty($subscriptionId)) { + return ['error' => true, 'msg' => 'Missing subscriptionId']; + } + + // Optional: verify subscription is still active + $statusCheck = self::isSubscriptionActive($subscriptionId); + if ($statusCheck['error']) { + return ['error' => true, 'msg' => 'Failed to check subscription status: ' . ($statusCheck['msg'] ?? '')]; + } + if (!$statusCheck['active']) { + return [ + 'error' => true, + 'active' => false, + 'status' => $statusCheck['status'] ?? '', + 'msg' => 'Subscription is not active' + ]; + } + + // Reuse the one-time processor for wallet + log + $res = self::processSinglePayment($users_id, $amount, $uniq_key, $eventType, $payload, $description); + if ($res['error']) { + return $res; + } + + // Attach status info for caller convenience + $res['active'] = true; + $res['status'] = $statusCheck['status'] ?? 'active'; + return $res; + } catch (Throwable $e) { + _error_log('[Authorize.Net] Exception in processSubscriptionCharge: ' . $e->getMessage()); + return ['error' => true, 'msg' => $e->getMessage()]; + } + } + + /** + * Process a subscription charge and associate it with a subscription plan. + * + * @param array $analysis Result from analyzeTransactionFromWebhook() + * @param string $uniq_key + * @param string $eventType + * @param array $payload + * @return array + */ + public static function processSubscriptionChargeWithPlan(array $analysis, string $uniq_key, string $eventType, array $payload): array + { + try { + if (empty($analysis['subscriptionId'])) { + return ['error' => true, 'msg' => 'Missing subscriptionId']; + } + + // First process the payment + $result = self::processSubscriptionCharge( + $analysis['subscriptionId'], + $analysis['users_id'], + $analysis['amount'], + $uniq_key, + $eventType, + $payload, + 'Authorize.Net subscription charge' + ); + + if ($result['error']) { + return $result; + } + + // If we have a plans_id, process the subscription plan + if (!empty($analysis['plans_id'])) { + try { + // Load subscription plan + require_once $global['systemRootPath'] . 'plugin/YPTWallet/Objects/SubscriptionPlansTable.php'; + $plan = new SubscriptionPlansTable($analysis['plans_id']); + + if (!empty($plan->getId())) { + // You might want to extend user subscription here + // This depends on your subscription management logic + _error_log("[AuthorizeNet] Processing subscription charge for plan: " . $analysis['plans_id']); + + $result['plans_id'] = $analysis['plans_id']; + $result['plan_name'] = $plan->getName(); + } + } catch (Exception $e) { + _error_log("[AuthorizeNet] Error processing subscription plan: " . $e->getMessage()); + // Don't fail the entire process if plan processing fails + } + } + + return $result; + } catch (Throwable $e) { + _error_log('[AuthorizeNet] Exception in processSubscriptionChargeWithPlan: ' . $e->getMessage()); + return ['error' => true, 'msg' => $e->getMessage()]; + } + } + + public static function getDefaultPaymentProfileId(string $customerProfileId): ?string + { + $profile = self::getCustomerProfile($customerProfileId); + if (empty($profile) || empty($profile['paymentProfiles'])) { + return null; + } + // try default first + foreach ($profile['paymentProfiles'] as $pp) { + if (!empty($pp['defaultPaymentProfile'])) { + return (string)$pp['customerPaymentProfileId']; + } + } + // otherwise first one + return (string)$profile['paymentProfiles'][0]['customerPaymentProfileId']; + } + + /** + * Create a recurring subscription (ARB) and store custom metadata. + * + * NOTE: ARB does not support arbitrary key/value pairs. + * I stuff metadata into: + * - Order::invoiceNumber (20 chars) → use it for plans_id or short code + * - Order::description (255 chars) → JSON-encoded metadata (trimmed) + * + * @param int $users_id + * @param float $amount + * @param array $metadata e.g. ['plans_id' => '123', 'subscription_name' => 'Premium'] + * @param int $intervalLength e.g. 1 + * @param string $intervalUnit 'months' or 'days' + * @param int $totalOccurrences Total number of charges (9999 = indefinite) + * @param string $startDate Start date (default: next interval) + * @return array{error:bool,msg?:string,subscriptionId?:string,storedMeta?:array} + */ + public static function createSubscription( + int $users_id, + float $amount, + array $metadata = [], + int $intervalLength = 1, + string $intervalUnit = 'days', + ): array { + $totalOccurrences = 9999; + try { + if ($amount <= 0) { + return ['error' => true, 'msg' => 'Invalid amount']; + } + if ($intervalLength <= 0) { + return ['error' => true, 'msg' => 'Invalid interval length']; + } + if ($intervalUnit !== 'months' && $intervalUnit !== 'days') { + return ['error' => true, 'msg' => 'Invalid interval unit (use months or days)']; + } + + // Ensure customer profile exists + $profileId = self::getOrCreateCustomerProfile($users_id); + if (empty($profileId)) { + return ['error' => true, 'msg' => 'Customer profile not found']; + } + + $customerPaymentProfileId = self::getDefaultPaymentProfileId($profileId); + if (empty($customerPaymentProfileId)) { + return ['error' => true, 'msg' => 'No payment profile on file. User must complete payment first.']; + } + + $merchantAuthentication = self::getMerchantAuthentication(); + $environment = self::getEnvironment(); + + // Build subscription object + $subscription = new ARBSubscriptionType(); + $subscriptionName = $metadata['subscription_name'] ?? "Subscription - User {$users_id} to plan {$metadata['plans_id']}"; + $subscription->setName($subscriptionName); + + // Interval / schedule + $interval = new IntervalAType(); + $interval->setLength($intervalLength); + $interval->setUnit($intervalUnit); + + $schedule = new PaymentScheduleType(); + $schedule->setInterval($interval); + + // Set start date + $startDate = date('Y-m-d', strtotime("+{$intervalLength} {$intervalUnit}")); + $schedule->setStartDate(new DateTime($startDate)); + $schedule->setTotalOccurrences($totalOccurrences); + + $subscription->setPaymentSchedule($schedule); + $subscription->setAmount($amount); + + // Attach profile (payment profile must already exist) + $profile = new CustomerProfileIdType(); + $profile->setCustomerProfileId($profileId); + $profile->setCustomerPaymentProfileId($customerPaymentProfileId); + $subscription->setProfile($profile); + + // ---- Metadata encoding ---- + $planId = $metadata['plans_id'] ?? ($metadata['plan_id'] ?? null); + + $order = new OrderType(); + if (!empty($planId)) { + // invoiceNumber length limit = 20 + $order->setInvoiceNumber(substr((string)$planId, 0, 20)); + } + + // description length limit = 255 + $metaJson = substr(json_encode($metadata, JSON_UNESCAPED_UNICODE), 0, 255); + $order->setDescription($metaJson); + + $subscription->setOrder($order); + + // Build request + $request = new ARBCreateSubscriptionRequest(); + $request->setMerchantAuthentication($merchantAuthentication); + $request->setSubscription($subscription); + + // Add a refId for logging (optional, 20 chars max) + $request->setRefId(substr('sub_' . $users_id . '_' . time(), 0, 20)); + + $controller = new ARBCreateSubscriptionController($request); + + /** @var ARBCreateSubscriptionResponse $response */ + $response = $controller->executeWithApiResponse($environment); + + if ( + $response && + $response->getMessages()->getResultCode() === 'Ok' && + method_exists($response, 'getSubscriptionId') && + $response->getSubscriptionId() + ) { + _error_log("[AuthorizeNet] Subscription created successfully: " . $response->getSubscriptionId()); + return [ + 'error' => false, + 'subscriptionId' => $response->getSubscriptionId(), + 'storedMeta' => ['invoiceNumber' => $order->getInvoiceNumber(), 'description' => $metaJson] + ]; + } + + return ['error' => true, 'msg' => self::extractSdkError($response)]; + } catch (Throwable $e) { + _error_log('[AuthorizeNet] Exception in createSubscription: ' . $e->getMessage()); + return ['error' => true, 'msg' => $e->getMessage()]; + } + } + + /** + * Check whether a subscription is active. + * + * @param string $subscriptionId + * @return array{error:bool,active?:bool,status?:string,msg?:string} + */ + public static function isSubscriptionActive(string $subscriptionId): array + { + if (trim($subscriptionId) === '') { + return ['error' => true, 'msg' => 'Missing subscriptionId']; + } + + try { + $merchantAuthentication = self::getMerchantAuthentication(); + $environment = self::getEnvironment(); + + $request = new ARBGetSubscriptionStatusRequest(); + $request->setMerchantAuthentication($merchantAuthentication); + $request->setSubscriptionId($subscriptionId); + + $controller = new ARBGetSubscriptionStatusController($request); + + /** @var ARBGetSubscriptionStatusResponse $response */ + $response = $controller->executeWithApiResponse($environment); + + if ( + $response && + $response->getMessages()->getResultCode() === 'Ok' && + method_exists($response, 'getStatus') + ) { + $status = (string) $response->getStatus(); + return [ + 'error' => false, + 'active' => ($status === 'active'), + 'status' => $status + ]; + } + + return ['error' => true, 'msg' => self::extractSdkError($response)]; + } catch (Throwable $e) { + _error_log('[AuthorizeNet] Exception in isSubscriptionActive: ' . $e->getMessage()); + return ['error' => true, 'msg' => $e->getMessage()]; + } + } + /** + * Generate Accept Hosted token (payment form). Returns token + redirect URL. + * Based on official sample: PaymentTransactions/get-hosted-payment-page.php + */ + public static function generateHostedPaymentPage(float $amount, array $metadata = [], string $currency = 'USD'): array + { + global $global; + + if ($amount <= 0) { + return ['error' => true, 'msg' => 'Invalid amount']; + } + self::ensureWebhookOrDie(); // make sure webhook exists or stop execution + // Optional: ensure webhook exists + $webhookCheck = self::createWebhookIfNotExists(); + _error_log('[AuthorizeNet] Webhook check: ' . json_encode($webhookCheck)); + if (!empty($webhookCheck['error']) && !empty($webhookCheck['msg'])) { + return ['error' => true, 'msg' => 'Webhook error: ' . $webhookCheck['msg']]; + } + + $merchantAuthentication = self::getMerchantAuthentication(); + $environment = self::getEnvironment(); + + $users_id = User::getId(); + $customerProfileId = self::getOrCreateCustomerProfile($users_id); + + _error_log('[AuthorizeNet] User ID: ' . $users_id); + _error_log('[AuthorizeNet] CustomerProfileId: ' . $customerProfileId); + + // Transaction + $txn = new AnetAPI\TransactionRequestType(); + $txn->setTransactionType('authCaptureTransaction'); + $txn->setAmount($amount); + $txn->setCurrencyCode($currency); + + $order = new AnetAPI\OrderType(); + $order->setInvoiceNumber(substr((string)$metadata['plans_id'], 0, 20)); + $order->setDescription(substr(json_encode($metadata, JSON_UNESCAPED_UNICODE), 0, 255)); + $txn->setOrder($order); + + foreach ($metadata as $k => $v) { + $uf = new AnetAPI\UserFieldType(); + $uf->setName($k); + $uf->setValue($v); + $txn->addToUserFields($uf); + } + + if (!empty($customerProfileId)) { + $profilePaymentType = new AnetAPI\CustomerProfilePaymentType(); + $profilePaymentType->setCustomerProfileId($customerProfileId); + $txn->setProfile($profilePaymentType); + _error_log('[AuthorizeNet] Attached CustomerProfileId to transaction: ' . $customerProfileId); + } else { + _error_log('[AuthorizeNet] No customer profile found for user ID: ' . $users_id); + } + + // Request + $request = new AnetAPI\GetHostedPaymentPageRequest(); + $request->setMerchantAuthentication($merchantAuthentication); + $request->setTransactionRequest($txn); + + // Settings + $settings = []; + + $returnOpt = new AnetAPI\SettingType(); + $returnOpt->setSettingName('hostedPaymentReturnOptions'); + $returnOpt->setSettingValue(json_encode([ + 'showReceipt' => false, + 'url' => "{$global['webSiteRootURL']}plugin/AuthorizeNet/acceptHostedReturn.php", + 'urlText' => 'Return to site', + 'cancelUrl' => "{$global['webSiteRootURL']}plugin/AuthorizeNet/acceptHostedReturn.php?cancel=1", + 'cancelUrlText' => 'Cancel' + ])); + $settings[] = $returnOpt; + + $createProfileSetting = new AnetAPI\SettingType(); + $createProfileSetting->setSettingName('hostedPaymentCustomerOptions'); + $createProfileSetting->setSettingValue(json_encode([ + 'showEmail' => false, + 'requiredEmail' => false, + 'addPaymentProfile' => true + ])); + $settings[] = $createProfileSetting; + + // Uncomment if using iframe + /* + $iframe = new AnetAPI\SettingType(); + $iframe->setSettingName('hostedPaymentIFrameCommunicatorUrl'); + $iframe->setSettingValue(json_encode([ + 'url' => $global['webSiteRootURL'] . 'plugin/AuthorizeNet/iframeCommunicator.html' + ])); + $settings[] = $iframe; + */ + + $request->setHostedPaymentSettings($settings); + + // Call API + $controller = new AnetController\GetHostedPaymentPageController($request); + /** @var GetHostedPaymentPageResponse $response */ + $response = $controller->executeWithApiResponse($environment); + _error_log('[AuthorizeNet] Payment Page API response: ' . json_encode($response)); + + $token = (method_exists($response, 'getToken')) ? $response->getToken() : null; + + if ($response && $response->getMessages()->getResultCode() === 'Ok' && !empty($token)) { + return [ + 'error' => false, + 'token' => $token, + 'url' => self::getHostedBaseUrl('/payment/payment') + ]; + } + + return ['error' => true, 'msg' => self::extractSdkError($response)]; + } + + /** + * Generate Accept Hosted token to manage card/profile. + */ + public static function generateManageProfileToken(): array + { + $merchantAuthentication = self::getMerchantAuthentication(); + $environment = self::getEnvironment(); + + $users_id = User::getId(); + $customerProfileId = self::getOrCreateCustomerProfile($users_id); + if (empty($customerProfileId)) { + return ['error' => true, 'msg' => 'No customer profile found']; + } + + $request = new AnetAPI\GetHostedProfilePageRequest(); + $request->setMerchantAuthentication($merchantAuthentication); + $request->setCustomerProfileId($customerProfileId); + + $controller = new AnetController\GetHostedProfilePageController($request); + /** @var GetHostedProfilePageResponse $response */ + $response = $controller->executeWithApiResponse($environment); + + $token = (method_exists($response, 'getToken')) ? $response->getToken() : null; + + if ($response && $response->getMessages()->getResultCode() === 'Ok' && !empty($token)) { + return [ + 'error' => false, + 'token' => $token, + 'url' => self::getHostedBaseUrl('/profile/manage') + ]; + } + + return ['error' => true, 'msg' => self::extractSdkError($response)]; + } + + public static function getCustomerProfileIdByMerchantCustomerId($merchantCustomerId) + { + if (empty($merchantCustomerId)) { + return false; + } + + $merchantAuthentication = self::getMerchantAuthentication(); + $environment = self::getEnvironment(); + + $request = new AnetAPI\GetCustomerProfileRequest(); + $request->setMerchantAuthentication($merchantAuthentication); + $request->setMerchantCustomerId($merchantCustomerId); + + $controller = new AnetController\GetCustomerProfileController($request); + /** @var GetCustomerProfileResponse $response */ + $response = $controller->executeWithApiResponse($environment); + + $profile = (method_exists($response, 'getProfile')) ? $response->getProfile() : null; + + if ( + $response && + $response->getMessages()->getResultCode() === 'Ok' && + $profile && + $profile->getCustomerProfileId() + ) { + return $profile->getCustomerProfileId(); + } + + _error_log("[AuthorizeNet] Failed to get CustomerProfileId by MerchantCustomerId: {$merchantCustomerId} | Error: " . self::extractSdkError($response)); + return false; + } + + + + /* ---------- Customer Profile ---------- */ + + public static function getOrCreateCustomerProfile(int $users_id) + { + $user = new User($users_id); + $profileId = $user->getExternalOption('authorizeNetcustomerProfileId'); + if (!empty($profileId)) { + _error_log("[AuthorizeNet] Using cached profileId {$profileId} for user {$users_id}"); + return $profileId; + } + + $merchantAuthentication = self::getMerchantAuthentication(); + + $customerProfile = new AnetAPI\CustomerProfileType(); + $customerProfile->setDescription('AVideo User ' . $users_id); + $customerProfile->setEmail($user->getEmail()); + $customerProfile->setMerchantCustomerId((string)$users_id); // force string + + $request = new AnetAPI\CreateCustomerProfileRequest(); + $request->setMerchantAuthentication($merchantAuthentication); + $request->setProfile($customerProfile); + + $controller = new AnetController\CreateCustomerProfileController($request); + /** @var CreateCustomerProfileResponse $response */ + $response = $controller->executeWithApiResponse(self::getEnvironment()); + + // Log everything + _error_log('[AuthorizeNet] CreateCustomerProfile RESPONSE: ' . json_encode($response)); + + if ($response instanceof CreateCustomerProfileResponse && $response->getMessages()->getResultCode() === 'Ok') { + $profileId = $response->getCustomerProfileId(); + _error_log("[AuthorizeNet] Profile created: {$profileId} for user {$users_id}"); + if (!empty($profileId)) { + $user->addExternalOptions('authorizeNetcustomerProfileId', $profileId); + return $profileId; + } + } else { + // Handle duplicate (E00039) or similar + $err = self::extractSdkError($response); + _error_log("[AuthorizeNet] CreateCustomerProfile ERROR: {$err}"); + + if (stripos($err, 'E00039') !== false) { // duplicate profile + $existing = self::getCustomerProfileIdByMerchantCustomerId((string)$users_id); + _error_log("[AuthorizeNet] Duplicate detected. Existing profileId: {$existing}"); + if (!empty($existing)) { + $user->addExternalOptions('authorizeNetcustomerProfileId', $existing); + return $existing; + } + } + } + return false; + } + + /** + * Get customer profile details including payment profiles + * + * @param string $customerProfileId + * @return array|null + */ + public static function getCustomerProfile(string $customerProfileId): ?array + { + try { + $merchantAuthentication = self::getMerchantAuthentication(); + + $request = new AnetAPI\GetCustomerProfileRequest(); + $request->setMerchantAuthentication($merchantAuthentication); + $request->setCustomerProfileId($customerProfileId); + + $controller = new AnetController\GetCustomerProfileController($request); + + /** @var GetCustomerProfileResponse|null $response */ + $response = $controller->executeWithApiResponse(self::getEnvironment()); + + // Make the static analyzer happy + runtime safe + if (!$response instanceof GetCustomerProfileResponse) { + _error_log('[AuthorizeNet] Empty/invalid profile response'); + return null; + } + + if ($response->getMessages()->getResultCode() !== 'Ok') { + $m = $response->getMessages()->getMessage(); + $err = isset($m[0]) ? ($m[0]->getCode() . ' ' . $m[0]->getText()) : 'Unknown error'; + _error_log("[AuthorizeNet] Error getting customer profile: $err"); + return null; + } + + $profile = $response->getProfile(); + if (!$profile) { + return null; + } + + $result = [ + 'customerProfileId' => $profile->getCustomerProfileId(), + 'merchantCustomerId' => $profile->getMerchantCustomerId(), + 'email' => $profile->getEmail(), + 'description' => $profile->getDescription(), + 'paymentProfiles' => [], + ]; + + $paymentProfiles = $profile->getPaymentProfiles(); + if (!empty($paymentProfiles)) { + foreach ($paymentProfiles as $pp) { + $result['paymentProfiles'][] = [ + 'customerPaymentProfileId' => $pp->getCustomerPaymentProfileId(), + 'defaultPaymentProfile' => (bool)$pp->getDefaultPaymentProfile(), + ]; + } + } + + return $result; + } catch (Throwable $e) { + _error_log("[AuthorizeNet] Exception getting customer profile: " . $e->getMessage()); + return null; + } + } + + /* ---------- Webhooks (REST) ---------- */ + + public static function createWebhook(string $webhookUrl, array $eventTypes = ['net.authorize.payment.authcapture.created']) + { + return self::restWebhook('POST', 'webhooks', [ + 'url' => $webhookUrl, + 'eventTypes' => $eventTypes, + 'status' => 'active', + ]); + } + + public static function webhookExists(string $webhookUrl) + { + $res = self::restWebhook('GET', 'webhooks'); + if ($res['error']) { + return $res; // real connection or auth error + } + + foreach ((array) $res['body'] as $wh) { + if (!empty($wh['url']) && $wh['url'] === $webhookUrl) { + $wh['error'] = false; + $wh['exists'] = true; + return $wh; + } + } + + // webhook not found but not an error + return ['error' => false, 'exists' => false]; + } + + public static function updateWebhookEventTypes(string $webhookId, array $eventTypes) + { + return self::restWebhook('PATCH', 'webhooks/' . $webhookId, ['eventTypes' => $eventTypes]); + } + + public static function createWebhookIfNotExists(array $eventTypes = ['net.authorize.payment.authcapture.created']) + { + $webhookUrl = AuthorizeNet::getWebhookURL(); + $exists = self::webhookExists($webhookUrl); + if (!empty($exists['error'])) { + return $exists; // real error + } + + // Create if it does not exist + if (empty($exists['exists'])) { + return self::createWebhook($webhookUrl, $eventTypes); + } + + // Already exists: check if update is needed + $existingEvents = $exists['eventTypes'] ?? []; + sort($existingEvents); + sort($eventTypes); + + if ($existingEvents === $eventTypes) { + return $exists; // already up to date + } + + if (!empty($exists['webhookId'])) { + return self::updateWebhookEventTypes($exists['webhookId'], $eventTypes); + } + + return ['error' => true, 'msg' => 'Webhook exists but missing ID', 'status' => 0]; + } + + private static function ensureWebhookOrDie(array $eventTypes = ['net.authorize.payment.authcapture.created']): void + { + $url = self::getWebhookURL(); + $res = self::createWebhookIfNotExists($eventTypes); + + if (!empty($res['error'])) { + self::abortWebhook('Authorize.Net webhook error: ' . ($res['msg'] ?? 'unknown')); + } + } + + /** + * Consulta detalhes de uma transação pelo ID. + * + * @param string $transactionId + * @return array{ + * error:bool, + * msg?:string, + * id?:string, + * status?:string|null, + * type?:string|null, + * amount?:float|null, + * currency?:string|null, + * responseCode?:int|string|null, + * authCode?:string|null, + * avsResponse?:string|null, + * email?:string|null, + * invoiceNumber?:string|null, + * submitTimeUTC?:string|null, + * raw?:mixed + * } + */ + public static function getTransactionDetails(string $transactionId): array + { + if (trim($transactionId) === '') { + return ['error' => true, 'msg' => 'Missing transactionId']; + } + + try { + $merchantAuthentication = self::getMerchantAuthentication(); + $environment = self::getEnvironment(); + + $request = new AnetAPI\GetTransactionDetailsRequest(); + $request->setMerchantAuthentication($merchantAuthentication); + $request->setTransId($transactionId); + + $controller = new AnetController\GetTransactionDetailsController($request); + + /** @var AnetAPI\GetTransactionDetailsResponse|null $response */ + $response = $controller->executeWithApiResponse($environment); + + if ( + $response instanceof AnetAPI\GetTransactionDetailsResponse && + $response->getMessages() && + $response->getMessages()->getResultCode() === 'Ok' && + method_exists($response, 'getTransaction') && + ($txn = $response->getTransaction()) + ) { + /** @var \net\authorize\api\contract\v1\TransactionDetailsType $txn */ + $order = method_exists($txn, 'getOrder') ? $txn->getOrder() : null; + $customer = method_exists($txn, 'getCustomer') ? $txn->getCustomer() : null; + $submitTime = method_exists($txn, 'getSubmitTimeUTC') ? $txn->getSubmitTimeUTC() : null; + $responseCode = $txn->getResponseCode() ?? ''; + $status = $txn->getTransactionStatus() ?? ''; + $isApproved = $responseCode == 1 && in_array($status, ['capturedPendingSettlement', 'settledSuccessfully'], true); + + // ---- NEW: get description and decode metadata ---- + $orderDescription = ($order && method_exists($order, 'getDescription')) ? (string)$order->getDescription() : null; + $decodedMeta = []; + if (!empty($orderDescription)) { + $tmp = json_decode($orderDescription, true); + if (is_array($tmp)) { + $decodedMeta = $tmp; + } + } + + return [ + 'error' => false, + 'id' => $transactionId, + 'status' => $status, + 'type' => $txn->getTransactionType(), + 'amount' => $txn->getAuthAmount(), + 'responseCode' => $responseCode, + 'authCode' => $txn->getAuthCode(), + 'avsResponse' => $txn->getAvsResponse(), + 'email' => $customer ? $customer->getEmail() : null, + 'invoiceNumber' => $order ? $order->getInvoiceNumber() : null, + 'orderDescription' => $orderDescription, + 'metadata' => $decodedMeta, // <- decoded JSON (if any) + 'submitTimeUTC' => $submitTime ? $submitTime->format('Y-m-d H:i:s') : null, + 'customer' => $txn->getCustomer(), + 'users_id' => $decodedMeta['users_id'] ?? $txn->getCustomer()->getId() ?? null, + 'plans_id' => $decodedMeta['plans_id'] ?? 0, + 'raw' => $txn, + 'isApproved' => $isApproved, + ]; + } + + return ['error' => true, 'msg' => self::extractSdkError($response)]; + } catch (Throwable $e) { + _error_log('[Authorize.Net] Exception in getTransactionDetails: ' . $e->getMessage()); + return ['error' => true, 'msg' => $e->getMessage()]; + } + } + + + + // 4) Stop execution and return JSON error response + private static function abortWebhook(string $msg): void + { + _error_log('[AuthorizeNet] ' . $msg); + http_response_code(500); + die(json_encode(['error' => true, 'msg' => $msg])); + } + + /* ---------- Helpers ---------- */ + + private static function getConfig() + { + return AVideoPlugin::getDataObject('AuthorizeNet'); + } + + private static function getMerchantAuthentication(): AnetAPI\MerchantAuthenticationType + { + $obj = self::getConfig(); + $auth = new AnetAPI\MerchantAuthenticationType(); + $auth->setName($obj->apiLoginId); + $auth->setTransactionKey($obj->transactionKey); + return $auth; + } + + private static function getEnvironment() + { + $obj = self::getConfig(); + return $obj->sandbox ? ANetEnvironment::SANDBOX : ANetEnvironment::PRODUCTION; + } + + /** + * Accept Hosted base (payment/profile). + */ + private static function getHostedBaseUrl(string $path): string + { + $base = self::getConfig()->sandbox + ? 'https://test.authorize.net' + : 'https://accept.authorize.net'; + return rtrim($base, '/') . $path; + } + + /** + * REST base (webhooks). + */ + private static function getRestBaseUrl(): string + { + return self::getConfig()->sandbox + ? 'https://apitest.authorize.net/rest/v1/' + : 'https://api.authorize.net/rest/v1/'; + } + + /** + * Webhook URL in your app. + */ + public static function getWebhookURL(): string + { + global $global; + return $global['webSiteRootURL'] . 'plugin/AuthorizeNet/webhook.php'; + } + + /** + * REST call for webhooks. Returns unified array with error flag. + */ + private static function restWebhook(string $method, string $path, ?array $payload = null): array + { + $url = self::getRestBaseUrl() . ltrim($path, '/'); + $obj = self::getConfig(); + + $headers = [ + 'Content-Type: application/json', + 'Authorization: Basic ' . base64_encode($obj->apiLoginId . ':' . $obj->transactionKey), + ]; + + $optsHttp = [ + 'method' => strtoupper($method), + 'header' => implode("\r\n", $headers) . "\r\n", + 'ignore_errors' => true, + ]; + if ($payload !== null) { + $optsHttp['content'] = json_encode($payload); + } + + $context = stream_context_create(['http' => $optsHttp]); + $raw = @file_get_contents($url, false, $context); + $status = 0; + $respHdr = $http_response_header ?? []; + + foreach ($respHdr as $hdr) { + if (preg_match('#HTTP/\d\.\d\s+(\d+)#', $hdr, $m)) { + $status = (int)$m[1]; + break; + } + } + + $body = json_decode($raw, true); + $errText = ($status < 200 || $status >= 300) + ? self::extractRestError(['status' => $status, 'body' => $body, 'raw' => $raw]) + : ''; + + return [ + 'status' => $status, + 'body' => $body, + 'raw' => $raw, + 'headers' => $respHdr, + 'error' => $status < 200 || $status >= 300, + 'msg' => $errText, + 'errorMsg' => $errText + ]; + } + + private static function extractSdkError($response): string + { + if (empty($response)) { + return 'Empty response'; + } + + if (method_exists($response, 'getMessages') && $response->getMessages()) { + $msgs = $response->getMessages()->getMessage(); + if (is_array($msgs) && !empty($msgs[0])) { + $m = $msgs[0]; + return trim(($m->getCode() ?? '') . ' ' . ($m->getText() ?? '')); + } + } + + if (method_exists($response, 'getTransactionResponse') && $response->getTransactionResponse()) { + $tr = $response->getTransactionResponse(); + if (method_exists($tr, 'getErrors') && $tr->getErrors()) { + $err = $tr->getErrors()[0]; + return trim(($err->getErrorCode() ?? '') . ' ' . ($err->getErrorText() ?? '')); + } + } + + return 'Unknown error'; + } + + private static function extractRestError(array $res): string + { + $body = $res['body'] ?? []; + if (is_array($body)) { + if (!empty($body['message'])) return $body['message']; + if (!empty($body['reason'])) return $body['reason']; + if (!empty($body['errors']) && is_array($body['errors'])) { + $first = reset($body['errors']); + return is_array($first) ? ($first['message'] ?? json_encode($first)) : (string)$first; + } + } + return $res['raw'] ?? 'Unknown error'; + } + + /* ---------- Plugin metadata ---------- */ + + public function getTags() + { + return [PluginTags::$MONETIZATION]; + } + + public function getDescription() + { + return "Authorize.Net payment gateway integration for AVideo."; + } + + public function getName() + { + return "AuthorizeNet"; + } + + public function getUUID() + { + return "authorizenet-uuid-001"; + } + + public function getPluginVersion() + { + return "1.0"; + } + + public function getPluginMenu() + { + global $global; + return ''; + } + + public function getEmptyDataObject() + { + $obj = new stdClass(); + $obj->apiLoginId = ""; + $obj->transactionKey = ""; + $obj->signatureKey = ""; + $obj->sandbox = true; + return $obj; + } + + /** + * Get all subscriptions for a customer profile + * + * @param string $customerProfileId + * @return array{error:bool,subscriptions:array,msg?:string} + */ + public static function getCustomerSubscriptions(string $customerProfileId): array + { + try { + if ($customerProfileId === '') { + return ['error' => true, 'msg' => 'Missing customer profile ID', 'subscriptions' => []]; + } + + $merchantAuthentication = self::getMerchantAuthentication(); + $environment = self::getEnvironment(); + + // Build request + $request = new AnetAPI\GetCustomerProfileRequest(); + $request->setMerchantAuthentication($merchantAuthentication); + $request->setCustomerProfileId($customerProfileId); + $request->setIncludeIssuerInfo(true); + + $controller = new AnetController\GetCustomerProfileController($request); + + /** @var GetCustomerProfileResponse|false $response */ + $response = $controller->executeWithApiResponse($environment); + + // Ensure type for static analyser and runtime safety + if (!$response instanceof GetCustomerProfileResponse) { + return ['error' => true, 'msg' => 'Invalid response type', 'subscriptions' => []]; + } + + if ($response->getMessages()->getResultCode() !== 'Ok') { + return ['error' => true, 'msg' => self::extractSdkError($response), 'subscriptions' => []]; + } + + /** @var \net\authorize\api\contract\v1\CustomerProfileMaskedType $profile */ + $profile = $response->getProfile(); + if (!$profile) { + return ['error' => false, 'subscriptions' => []]; + } + + // It might return an array or an object that implements Traversable – cast to array for safety + $subscriptionIds = []; + if (method_exists($profile, 'getSubscriptionIds')) { + $subscriptionIds = (array) $profile->getSubscriptionIds(); + } elseif (method_exists($response, 'getSubscriptionIds')) { // older binding + $subscriptionIds = (array) $response->getSubscriptionIds(); + } + + + $subscriptions = []; + + foreach ($subscriptionIds as $subId) { + $subDetails = self::getSubscriptionDetails((string)$subId); + if (empty($subDetails['error']) && !empty($subDetails['subscription'])) { + // Store only the subscription array, not the whole wrapper + $subscriptions[] = $subDetails['subscription']; + } + } + + return ['error' => false, 'subscriptions' => $subscriptions]; + } catch (Throwable $e) { + _error_log('[AuthorizeNet] Exception in getCustomerSubscriptions: ' . $e->getMessage()); + return ['error' => true, 'msg' => $e->getMessage(), 'subscriptions' => []]; + } + } + + /** + * Get detailed subscription information + * + * @param string $subscriptionId + * @return array{error:bool,subscription?:array,msg?:string} + */ + public static function getSubscriptionDetails(string $subscriptionId): array + { + try { + if ($subscriptionId === '') { + return ['error' => true, 'msg' => 'Missing subscription ID']; + } + + $merchantAuthentication = self::getMerchantAuthentication(); + $environment = self::getEnvironment(); + + $request = new ARBGetSubscriptionRequest(); + $request->setMerchantAuthentication($merchantAuthentication); + $request->setSubscriptionId($subscriptionId); + + $controller = new ARBGetSubscriptionController($request); + + /** @var ARBGetSubscriptionResponse|false $response */ + $response = $controller->executeWithApiResponse($environment); + + // Static analyser + runtime check + if (!$response instanceof ARBGetSubscriptionResponse) { + return ['error' => true, 'msg' => 'Invalid subscription response']; + } + + if ($response->getMessages()->getResultCode() !== 'Ok') { + return ['error' => true, 'msg' => self::extractSdkError($response)]; + } + + /** @var \net\authorize\api\contract\v1\ARBSubscriptionMaskedType $subscription */ + $subscription = $response->getSubscription(); + if (!$subscription) { + return ['error' => true, 'msg' => 'Subscription not found']; + } + + // Safely unwrap schedule and order (they can be null) + $schedule = $subscription->getPaymentSchedule(); + $interval = $schedule ? $schedule->getInterval() : null; + + $details = [ + 'subscriptionId' => $subscriptionId, + 'name' => $subscription->getName(), + 'status' => $subscription->getStatus(), + 'amount' => $subscription->getAmount(), + 'interval' => [ + 'length' => $interval ? $interval->getLength() : null, + 'unit' => $interval ? $interval->getUnit() : null, + ], + 'startDate' => $schedule && $schedule->getStartDate() + ? $schedule->getStartDate()->format('Y-m-d') + : null, + 'totalOccurrences' => $schedule ? $schedule->getTotalOccurrences() : null, + 'trialOccurrences' => $schedule ? $schedule->getTrialOccurrences() : null, + 'order' => null, + 'metadata' => [], + 'plans_id' => null, + ]; + + $order = $subscription->getOrder(); + if ($order) { + $details['order'] = [ + 'invoiceNumber' => $order->getInvoiceNumber(), + 'description' => $order->getDescription() + ]; + + // Try to decode metadata from description + if ($order->getDescription()) { + $decodedMeta = json_decode($order->getDescription(), true); + if (is_array($decodedMeta)) { + $details['metadata'] = $decodedMeta; + $details['plans_id'] = $decodedMeta['plans_id'] ?? $order->getInvoiceNumber(); + } else { + $details['plans_id'] = $order->getInvoiceNumber(); + } + } else { + $details['plans_id'] = $order->getInvoiceNumber(); + } + } + + return ['error' => false, 'subscription' => $details]; + } catch (Throwable $e) { + _error_log('[AuthorizeNet] Exception in getSubscriptionDetails: ' . $e->getMessage()); + return ['error' => true, 'msg' => $e->getMessage()]; + } + } + + + /** + * Check if user has an active subscription for a specific plan + * + * @param int $users_id + * @param string|null $plans_id Optional: check for specific plan + * @return array{error:bool,hasActiveSubscription:bool,activeSubscriptions:array,msg?:string} + */ + public static function checkUserActiveSubscriptions(int $users_id, ?string $plans_id = null): array + { + try { + $customerProfileId = self::getOrCreateCustomerProfile($users_id); + if (empty($customerProfileId)) { + return ['error' => true, 'msg' => 'Customer profile not found']; + } + + $subscriptionsResult = self::getCustomerSubscriptions($customerProfileId); + if ($subscriptionsResult['error']) { + return $subscriptionsResult; + } + + $activeSubscriptions = []; + $hasActiveSubscription = false; + $hasActivePlanSubscription = false; + + foreach ($subscriptionsResult['subscriptions'] as $sub) { + if (!$sub['error'] && isset($sub['subscription'])) { + $subscription = $sub['subscription']; + + // Check if subscription is active + if (strtolower($subscription['status']) === 'active') { + $activeSubscriptions[] = $subscription; + $hasActiveSubscription = true; + + // Check if it's for the specific plan + if ( + $plans_id !== null && isset($subscription['plans_id']) && + $subscription['plans_id'] == $plans_id + ) { + $hasActivePlanSubscription = true; + } + } + } + } + + return [ + 'error' => false, + 'hasActiveSubscription' => $hasActiveSubscription, + 'hasActivePlanSubscription' => $hasActivePlanSubscription, + 'activeSubscriptions' => $activeSubscriptions, + 'totalSubscriptions' => count($subscriptionsResult['subscriptions']) + ]; + } catch (Throwable $e) { + _error_log('[AuthorizeNet] Exception in checkUserActiveSubscriptions: ' . $e->getMessage()); + return ['error' => true, 'msg' => $e->getMessage()]; + } + } + + /** + * Cancel a subscription + * + * @param string $subscriptionId + * @return array{error:bool,msg?:string,status?:string} + */ + public static function cancelSubscription(string $subscriptionId): array + { + try { + if (trim($subscriptionId) === '') { + return ['error' => true, 'msg' => 'Missing subscriptionId']; + } + + $merchantAuthentication = self::getMerchantAuthentication(); + $environment = self::getEnvironment(); + + $request = new \net\authorize\api\contract\v1\ARBCancelSubscriptionRequest(); + $request->setMerchantAuthentication($merchantAuthentication); + $request->setSubscriptionId($subscriptionId); + + $controller = new \net\authorize\api\controller\ARBCancelSubscriptionController($request); + $response = $controller->executeWithApiResponse($environment); + + if ( + $response && + $response->getMessages()->getResultCode() === 'Ok' + ) { + _error_log("[AuthorizeNet] Subscription canceled successfully: " . $subscriptionId); + return [ + 'error' => false, + 'msg' => 'Subscription canceled successfully', + 'status' => 'canceled' + ]; + } + + return ['error' => true, 'msg' => self::extractSdkError($response)]; + } catch (Throwable $e) { + _error_log('[AuthorizeNet] Exception in cancelSubscription: ' . $e->getMessage()); + return ['error' => true, 'msg' => $e->getMessage()]; + } + } + + /** + * Get all active subscriptions for a user from Authorize.Net API + * + * @param int $users_id + * @return array{error:bool,subscriptions:array,msg?:string} + */ + public static function getUserActiveSubscriptions(int $users_id): array + { + try { + $customerProfileId = self::getOrCreateCustomerProfile($users_id); + if (empty($customerProfileId)) { + return ['error' => true, 'msg' => 'Customer profile not found', 'subscriptions' => []]; + } + + $subscriptionsResult = self::getCustomerSubscriptions($customerProfileId); + if ($subscriptionsResult['error']) { + return $subscriptionsResult; + } + + $activeSubscriptions = []; + foreach ($subscriptionsResult['subscriptions'] as $sub) { + if (isset($sub['status']) && strtolower($sub['status']) === 'active') { + // Get detailed subscription info including current status from API + $detailsResult = self::getSubscriptionDetails($sub['subscriptionId']); + if (!$detailsResult['error'] && !empty($detailsResult['subscription'])) { + $activeSubscriptions[] = $detailsResult['subscription']; + } + } + } + + return ['error' => false, 'subscriptions' => $activeSubscriptions]; + } catch (Throwable $e) { + _error_log('[AuthorizeNet] Exception in getUserActiveSubscriptions: ' . $e->getMessage()); + return ['error' => true, 'msg' => $e->getMessage(), 'subscriptions' => []]; + } + } + + /** + * Get subscription by ID with current status from API + * + * @param string $subscriptionId + * @return array{error:bool,subscription?:array,msg?:string} + */ + public static function getSubscriptionWithCurrentStatus(string $subscriptionId): array + { + try { + // First get basic subscription details + $detailsResult = self::getSubscriptionDetails($subscriptionId); + if ($detailsResult['error']) { + return $detailsResult; + } + + // Then get current status + $statusResult = self::isSubscriptionActive($subscriptionId); + if ($statusResult['error']) { + return $statusResult; + } + + $subscription = $detailsResult['subscription']; + $subscription['currentStatus'] = $statusResult['status']; + $subscription['isActive'] = $statusResult['active']; + + return ['error' => false, 'subscription' => $subscription]; + } catch (Throwable $e) { + _error_log('[AuthorizeNet] Exception in getSubscriptionWithCurrentStatus: ' . $e->getMessage()); + return ['error' => true, 'msg' => $e->getMessage()]; + } + } +} diff --git a/plugin/AuthorizeNet/Objects/Anet_webhook_log.php b/plugin/AuthorizeNet/Objects/Anet_webhook_log.php new file mode 100644 index 0000000000..191f551245 --- /dev/null +++ b/plugin/AuthorizeNet/Objects/Anet_webhook_log.php @@ -0,0 +1,156 @@ +id = intval($id); + } + function setUniq_key($uniq_key) + { + $this->uniq_key = $uniq_key; + } + function setEvent_type($event_type) + { + $this->event_type = $event_type; + } + function setTrans_id($trans_id) + { + $this->trans_id = $trans_id; + } + function setPayload_json($payload_json) + { + $this->payload_json = $payload_json; + } + function setProcessed($processed) + { + $this->processed = intval($processed); + } + function setError_text($error_text) + { + $this->error_text = $error_text; + } + function setStatus($status) + { + $this->status = $status; + } + function setCreated_php_time($created_php_time) + { + $this->created_php_time = $created_php_time; + } + function setModified_php_time($modified_php_time) + { + $this->modified_php_time = $modified_php_time; + } + function setUsers_id($users_id) + { + $this->users_id = intval($users_id); + } + + function getId() + { + return intval($this->id); + } + function getUniq_key() + { + return $this->uniq_key; + } + function getEvent_type() + { + return $this->event_type; + } + function getTrans_id() + { + return $this->trans_id; + } + function getPayload_json() + { + return $this->payload_json; + } + function getProcessed() + { + return intval($this->processed); + } + function getError_text() + { + return $this->error_text; + } + function getStatus() + { + return $this->status; + } + function getCreated_php_time() + { + return $this->created_php_time; + } + function getModified_php_time() + { + return $this->modified_php_time; + } + function getUsers_id() + { + return intval($this->users_id); + } + + public static function alreadyProcessed($uniq_key) + { + if (empty($uniq_key)) { + return false; + } + $obj = self::getFromUniqKey($uniq_key); + if (!empty($obj) && !empty($obj['processed'])) { + return true; + } + return false; + } + + public static function getFromUniqKey($uniq_key) + { + global $global; + $sql = "SELECT * FROM " . static::getTableName() . " WHERE uniq_key = ? LIMIT 1"; + $res = sqlDAL::readSql($sql, "s", [$uniq_key]); + $data = sqlDAL::fetchAssoc($res); + sqlDAL::close($res); + if ($res) { + return $data; + } + return false; + } + + public static function createIfNotExists($uniq_key, $event_type, $payload_json, $users_id = 0) + { + if (self::alreadyProcessed($uniq_key)) { + return false; + } + + if(empty($users_id)){ + $users_id = User::getId(); + } + + $obj = new self(); + $obj->setUniq_key($uniq_key); + $obj->setEvent_type($event_type); + $obj->setPayload_json(_json_encode($payload_json)); + $obj->setUsers_id($users_id); + $obj->setProcessed(0); + $obj->setCreated_php_time(time()); + $obj->setModified_php_time(time()); + + return $obj->save(); + } +} diff --git a/plugin/AuthorizeNet/View/Anet_webhook_log/add.json.php b/plugin/AuthorizeNet/View/Anet_webhook_log/add.json.php new file mode 100644 index 0000000000..39e73054e1 --- /dev/null +++ b/plugin/AuthorizeNet/View/Anet_webhook_log/add.json.php @@ -0,0 +1,33 @@ +error = true; +$obj->msg = ""; + +$plugin = AVideoPlugin::loadPluginIfEnabled('AuthorizeNet'); + +if(!User::isAdmin()){ + $obj->msg = "You cant do this"; + die(json_encode($obj)); +} + +$o = new Anet_webhook_log(@$_POST['id']); +$o->setUniq_key($_POST['uniq_key']); +$o->setEvent_type($_POST['event_type']); +$o->setTrans_id($_POST['trans_id']); +$o->setPayload_json($_POST['payload_json']); +$o->setProcessed($_POST['processed']); +$o->setError_text($_POST['error_text']); +$o->setStatus($_POST['status']); +$o->setCreated_php_time($_POST['created_php_time']); +$o->setModified_php_time($_POST['modified_php_time']); +$o->setUsers_id($_POST['users_id']); + +if($id = $o->save()){ + $obj->error = false; +} + +echo json_encode($obj); diff --git a/plugin/AuthorizeNet/View/Anet_webhook_log/delete.json.php b/plugin/AuthorizeNet/View/Anet_webhook_log/delete.json.php new file mode 100644 index 0000000000..bec4b086fd --- /dev/null +++ b/plugin/AuthorizeNet/View/Anet_webhook_log/delete.json.php @@ -0,0 +1,20 @@ +error = true; + +$plugin = AVideoPlugin::loadPluginIfEnabled('AuthorizeNet'); + +if(!User::isAdmin()){ + $obj->msg = "You cant do this"; + die(json_encode($obj)); +} + +$id = intval($_POST['id']); +$row = new Anet_webhook_log($id); +$obj->error = !$row->delete(); +die(json_encode($obj)); +?> \ No newline at end of file diff --git a/plugin/AuthorizeNet/View/Anet_webhook_log/index.php b/plugin/AuthorizeNet/View/Anet_webhook_log/index.php new file mode 100644 index 0000000000..716abda7b7 --- /dev/null +++ b/plugin/AuthorizeNet/View/Anet_webhook_log/index.php @@ -0,0 +1,19 @@ +setExtraStyles(array('view/css/DataTables/datatables.min.css', 'view/js/bootstrap-datetimepicker/css/bootstrap-datetimepicker.min.css')); +$_page->setExtraScripts(array('view/css/DataTables/datatables.min.js')); +include $global['systemRootPath'] . 'plugin/AuthorizeNet/View/Anet_webhook_log/index_body.php'; +$_page->print(); +?> \ No newline at end of file diff --git a/plugin/AuthorizeNet/View/Anet_webhook_log/index_body.php b/plugin/AuthorizeNet/View/Anet_webhook_log/index_body.php new file mode 100644 index 0000000000..0de17fe75e --- /dev/null +++ b/plugin/AuthorizeNet/View/Anet_webhook_log/index_body.php @@ -0,0 +1,169 @@ + +
+
+
+ +
+
+
+
+
+
+
+
+
+ +
+
+ + +
+
+
+
+
+
+
+
+
+
+
+ + + + + + + + + + + + + +
+
+
+
+
+
+
+ +
+ diff --git a/plugin/AuthorizeNet/View/Anet_webhook_log/list.json.php b/plugin/AuthorizeNet/View/Anet_webhook_log/list.json.php new file mode 100644 index 0000000000..04a491e670 --- /dev/null +++ b/plugin/AuthorizeNet/View/Anet_webhook_log/list.json.php @@ -0,0 +1,16 @@ + $rows, + 'draw' => intval(@$_REQUEST['draw']), + 'recordsTotal' => $total, + 'recordsFiltered' => $total, +); +echo _json_encode($response); +?> \ No newline at end of file diff --git a/plugin/AuthorizeNet/View/editor.php b/plugin/AuthorizeNet/View/editor.php new file mode 100644 index 0000000000..e786c66532 --- /dev/null +++ b/plugin/AuthorizeNet/View/editor.php @@ -0,0 +1,29 @@ +setExtraStyles(array('view/css/DataTables/datatables.min.css', 'view/js/bootstrap-datetimepicker/css/bootstrap-datetimepicker.min.css')); +$_page->setExtraScripts(array('view/css/DataTables/datatables.min.js', 'view/js/bootstrap-datetimepicker/js/bootstrap-datetimepicker.min.js')); +?> +
+
+
+
+ +
+
+
+ +
+
+ +
+
+
+
+
+print(); +?> \ No newline at end of file diff --git a/plugin/AuthorizeNet/acceptHostedReturn.php b/plugin/AuthorizeNet/acceptHostedReturn.php new file mode 100644 index 0000000000..dbbceaf765 --- /dev/null +++ b/plugin/AuthorizeNet/acceptHostedReturn.php @@ -0,0 +1,90 @@ + [ + 'title' => 'Payment Successful', + 'icon' => 'fa-check-circle', + 'alert' => 'alert-success', + 'button' => 'btn-success', + 'progress' => 'progress-bar-success', + 'text' => 'Your payment has been processed successfully. This window will close automatically.' + ], + 'cancel' => [ + 'title' => 'Payment Cancelled', + 'icon' => 'fa-times-circle', + 'alert' => 'alert-warning', + 'button' => 'btn-warning', + 'progress' => 'progress-bar-warning', + 'text' => 'Your payment was not completed. You can try again or contact support if needed.' + ] +]; + +$_page = new Page([$messages[$type]['title']]); +$_page->setIncludeNavbar(false); +$_page->setIncludeFooter(false); +?> + +
+
+

+ + +

+ +

+ +

+ Closing in 10 seconds... +

+ +
+
+
+
+ + +
+
+ +print(); diff --git a/plugin/AuthorizeNet/cancelSubscription.json.php b/plugin/AuthorizeNet/cancelSubscription.json.php new file mode 100644 index 0000000000..ba2b8276a7 --- /dev/null +++ b/plugin/AuthorizeNet/cancelSubscription.json.php @@ -0,0 +1,86 @@ +error = true; +$obj->msg = ""; + +try { + // Check if user is logged in + if (!User::isLogged()) { + $obj->msg = "You must be logged in to cancel subscriptions"; + die(json_encode($obj)); + } + + // Check if AuthorizeNet plugin is enabled + $plugin = AVideoPlugin::loadPluginIfEnabled('AuthorizeNet'); + if (empty($plugin)) { + $obj->msg = "AuthorizeNet plugin is disabled"; + die(json_encode($obj)); + } + + // Get subscription ID from POST data + $subscriptionId = $_POST['subscriptionId'] ?? ''; + + if (empty($subscriptionId)) { + $obj->msg = "Missing subscription ID"; + die(json_encode($obj)); + } + + // Verify that this subscription belongs to the current user + $users_id = User::getId(); + $customerProfileId = AuthorizeNet::getOrCreateCustomerProfile($users_id); + + if (empty($customerProfileId)) { + $obj->msg = "Customer profile not found"; + die(json_encode($obj)); + } + + // Get subscription details to verify ownership + $subscriptionResult = AuthorizeNet::getSubscriptionWithCurrentStatus($subscriptionId); + + if ($subscriptionResult['error']) { + $obj->msg = "Failed to verify subscription: " . $subscriptionResult['msg']; + die(json_encode($obj)); + } + + // Additional security check: verify the subscription belongs to this customer + $userSubscriptions = AuthorizeNet::getUserActiveSubscriptions($users_id); + $subscriptionFound = false; + + if (!$userSubscriptions['error']) { + foreach ($userSubscriptions['subscriptions'] as $sub) { + if ($sub['subscriptionId'] === $subscriptionId) { + $subscriptionFound = true; + break; + } + } + } + + if (!$subscriptionFound) { + $obj->msg = "Subscription not found or does not belong to current user"; + die(json_encode($obj)); + } + + // Cancel the subscription + $cancelResult = AuthorizeNet::cancelSubscription($subscriptionId); + + if ($cancelResult['error']) { + $obj->msg = $cancelResult['msg']; + die(json_encode($obj)); + } + + // Success response + $obj->error = false; + $obj->msg = "Subscription canceled successfully"; + $obj->subscriptionId = $subscriptionId; + $obj->status = $cancelResult['status']; + +} catch (Exception $e) { + $obj->msg = "An error occurred: " . $e->getMessage(); + _error_log("[AuthorizeNet] Error in cancelSubscription.json.php: " . $e->getMessage()); +} + +echo json_encode($obj); +?> diff --git a/plugin/AuthorizeNet/cancelSubscription.php b/plugin/AuthorizeNet/cancelSubscription.php new file mode 100644 index 0000000000..ca815ef613 --- /dev/null +++ b/plugin/AuthorizeNet/cancelSubscription.php @@ -0,0 +1,496 @@ + + +
+
+
+

+ + +

+
+
+ + + +
+
+
+

+ + + + + + +

+ +
+
+

:
+

+
+
+

:
+ $

+
+
+ +
+
+

:
+ Every +

+
+ +
+

:
+

+
+ +
+ + +
+ +
+

:
+

+
+ + + +
+

:
+

+
+ +
+ +
+ +
+ + + +
+ + +
+ + + +
+
+
+ +
+ + + +
+ + + +
+
+ +

+
+
+ +
+ +
+ + +
+
+
+ + + +print(); +?> diff --git a/plugin/AuthorizeNet/getAcceptHostedToken.json.php b/plugin/AuthorizeNet/getAcceptHostedToken.json.php new file mode 100644 index 0000000000..13b22d9162 --- /dev/null +++ b/plugin/AuthorizeNet/getAcceptHostedToken.json.php @@ -0,0 +1,67 @@ +getPrice(); + } + if(empty($amount)){ + $amount = isset($_REQUEST['amount']) ? floatval($_REQUEST['amount']) : 0; + } + if ($amount <= 0) { + echo json_encode(['error' => true, 'msg' => 'Invalid amount', 'line' => __LINE__]); + exit; + } + + // ========== Add optional metadata ========== + $metadata = []; + $metadata['users_id'] = User::getId(); + $metadata['plans_id'] = $_REQUEST['plans_id'] ?? 0; + + AuthorizeNet::createWebhookIfNotExists(); + + // ========== Process payment via SDK using Accept opaque token + metadata ========== + $result = AuthorizeNet::generateHostedPaymentPage($amount, $metadata); + if (!empty($result['success'])) { + echo json_encode([ + 'error' => false, + 'msg' => 'Payment created successfully', + 'transactionId' => $result['transactionId'], + 'line' => __LINE__ + ]); + exit; + } + + // ========== Return error response if payment fails ========== + echo json_encode([ + 'error' => !isset($result['error']) || !empty($result['error']), + 'msg' => $result['msg'] ?? '', + 'result' => $result, + 'line' => __LINE__, + 'url' => $result['url'] ?? '', + 'token' => $result['token'] ?? '', + ]); + exit; + +} catch (Exception $e) { + // ========== Return exception error ========== + echo json_encode([ + 'error' => true, + 'msg' => $e->getMessage(), + 'line' => __LINE__ + ]); + exit; +} diff --git a/plugin/AuthorizeNet/getProfileManager.json.php b/plugin/AuthorizeNet/getProfileManager.json.php new file mode 100644 index 0000000000..bdb4c06128 --- /dev/null +++ b/plugin/AuthorizeNet/getProfileManager.json.php @@ -0,0 +1,19 @@ + true, + 'msg' => $e->getMessage(), + 'line' => __LINE__ + ]); + exit; +} diff --git a/plugin/AuthorizeNet/getSubscriptionStatus.json.php b/plugin/AuthorizeNet/getSubscriptionStatus.json.php new file mode 100644 index 0000000000..c8a8334c25 --- /dev/null +++ b/plugin/AuthorizeNet/getSubscriptionStatus.json.php @@ -0,0 +1,78 @@ +error = true; +$obj->msg = ""; + +try { + // Check if user is logged in + if (!User::isLogged()) { + $obj->msg = "You must be logged in to check subscription status"; + die(json_encode($obj)); + } + + // Check if AuthorizeNet plugin is enabled + $plugin = AVideoPlugin::loadPluginIfEnabled('AuthorizeNet'); + if (empty($plugin)) { + $obj->msg = "AuthorizeNet plugin is disabled"; + die(json_encode($obj)); + } + + // Get subscription ID from GET data + $subscriptionId = $_GET['subscriptionId'] ?? ''; + + if (empty($subscriptionId)) { + $obj->msg = "Missing subscription ID"; + die(json_encode($obj)); + } + + // Verify ownership (similar to cancel endpoint) + $users_id = User::getId(); + $customerProfileId = AuthorizeNet::getOrCreateCustomerProfile($users_id); + + if (empty($customerProfileId)) { + $obj->msg = "Customer profile not found"; + die(json_encode($obj)); + } + + // Get subscription with current status + $subscriptionResult = AuthorizeNet::getSubscriptionWithCurrentStatus($subscriptionId); + + if ($subscriptionResult['error']) { + $obj->msg = $subscriptionResult['msg']; + die(json_encode($obj)); + } + + // Verify ownership + $userSubscriptions = AuthorizeNet::getUserActiveSubscriptions($users_id); + $subscriptionFound = false; + + if (!$userSubscriptions['error']) { + foreach ($userSubscriptions['subscriptions'] as $sub) { + if ($sub['subscriptionId'] === $subscriptionId) { + $subscriptionFound = true; + break; + } + } + } + + if (!$subscriptionFound) { + $obj->msg = "Subscription not found or does not belong to current user"; + die(json_encode($obj)); + } + + // Success response + $obj->error = false; + $obj->subscription = $subscriptionResult['subscription']; + $obj->status = $subscriptionResult['subscription']['currentStatus']; + $obj->isActive = $subscriptionResult['subscription']['isActive']; + +} catch (Exception $e) { + $obj->msg = "An error occurred: " . $e->getMessage(); + _error_log("[AuthorizeNet] Error in getSubscriptionStatus.json.php: " . $e->getMessage()); +} + +echo json_encode($obj); +?> diff --git a/plugin/AuthorizeNet/getSubscriptions.json.php b/plugin/AuthorizeNet/getSubscriptions.json.php new file mode 100644 index 0000000000..8da1bdb7a0 --- /dev/null +++ b/plugin/AuthorizeNet/getSubscriptions.json.php @@ -0,0 +1,72 @@ +error = true; +$obj->msg = ""; +$obj->subscriptions = []; + +try { + // Check if user is logged in + if (!User::isLogged()) { + $obj->msg = "You must be logged in to view subscriptions"; + die(json_encode($obj)); + } + + // Check if AuthorizeNet plugin is enabled + $plugin = AVideoPlugin::loadPluginIfEnabled('AuthorizeNet'); + if (empty($plugin)) { + $obj->msg = "AuthorizeNet plugin is disabled"; + die(json_encode($obj)); + } + + $users_id = User::getId(); + + // Get customer profile + $customerProfileId = AuthorizeNet::getOrCreateCustomerProfile($users_id); + + if (empty($customerProfileId)) { + $obj->msg = "Customer profile not found"; + die(json_encode($obj)); + } + + // Get all subscriptions for this customer from Authorize.Net API + $subscriptionsResult = AuthorizeNet::getCustomerSubscriptions($customerProfileId); + + if ($subscriptionsResult['error']) { + $obj->msg = $subscriptionsResult['msg']; + die(json_encode($obj)); + } + + $subscriptionsWithStatus = []; + + // Get current status for each subscription + foreach ($subscriptionsResult['subscriptions'] as $subscription) { + $subscriptionId = $subscription['subscriptionId']; + + // Get detailed subscription info with current status + $detailsResult = AuthorizeNet::getSubscriptionWithCurrentStatus($subscriptionId); + + if (!$detailsResult['error'] && !empty($detailsResult['subscription'])) { + $subscriptionsWithStatus[] = $detailsResult['subscription']; + } + } + + // Sort subscriptions by creation date (newest first) + usort($subscriptionsWithStatus, function($a, $b) { + return $b['subscriptionId'] - $a['subscriptionId']; + }); + + // Success response + $obj->error = false; + $obj->subscriptions = $subscriptionsWithStatus; + $obj->total = count($subscriptionsWithStatus); + +} catch (Exception $e) { + $obj->msg = "An error occurred: " . $e->getMessage(); + _error_log("[AuthorizeNet] Error in getSubscriptions.json.php: " . $e->getMessage()); +} + +echo json_encode($obj); +?> diff --git a/plugin/AuthorizeNet/install/install.sql b/plugin/AuthorizeNet/install/install.sql new file mode 100644 index 0000000000..f04cdaa21f --- /dev/null +++ b/plugin/AuthorizeNet/install/install.sql @@ -0,0 +1,22 @@ +CREATE TABLE IF NOT EXISTS `anet_webhook_log` ( + `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, + `uniq_key` VARCHAR(120) NOT NULL, + `event_type` VARCHAR(120) NOT NULL, + `trans_id` VARCHAR(45) NULL, + `payload_json` JSON NOT NULL, + `processed` TINYINT(1) NOT NULL DEFAULT 0, + `error_text` TEXT NULL, + `status` VARCHAR(45) NULL, + `created_php_time` BIGINT NULL, + `modified_php_time` BIGINT NULL, + `users_id` INT(11) NOT NULL, + PRIMARY KEY (`id`), + UNIQUE INDEX `uniq_key_UNIQUE` (`uniq_key` ASC), + INDEX `fk_anet_webhook_log_users1_idx` (`users_id` ASC), + INDEX `event_type_index` (`event_type` ASC), + CONSTRAINT `fk_anet_webhook_log_users1` + FOREIGN KEY (`users_id`) + REFERENCES `users` (`id`) + ON DELETE NO ACTION + ON UPDATE NO ACTION) +ENGINE = InnoDB; diff --git a/plugin/AuthorizeNet/processPayment.json.php b/plugin/AuthorizeNet/processPayment.json.php new file mode 100644 index 0000000000..905bfa8080 --- /dev/null +++ b/plugin/AuthorizeNet/processPayment.json.php @@ -0,0 +1,29 @@ + 'Invalid amount']); + exit; +} +// TODO: Implement payment logic using Authorize.Net API +// Example: Call Authorize.Net API here +// $result = $plugin->chargePayment($amount, $userData); + +// Simulate payment success for now +$paymentSuccess = true; +$users_id = @User::getId(); +if ($paymentSuccess && !empty($users_id)) { + // Add funds to wallet + $walletPlugin = AVideoPlugin::loadPluginIfEnabled("YPTWallet"); + if ($walletPlugin) { + $walletPlugin->addBalance($users_id, $amount, 'Authorize.Net one-time payment'); + echo json_encode(['success' => true, 'result' => 'Payment processed and wallet updated']); + exit; + } +} +echo json_encode(['error' => 'Payment failed or user not logged in']); diff --git a/plugin/AuthorizeNet/webhook.php b/plugin/AuthorizeNet/webhook.php new file mode 100644 index 0000000000..66be0a91da --- /dev/null +++ b/plugin/AuthorizeNet/webhook.php @@ -0,0 +1,140 @@ + false, + 'subscriptionId' => 'existing', + 'msg' => 'User already has active subscription for this plan', + 'existingSubscriptions' => $existingCheck['activeSubscriptions'] + ]; + } else { + + $sp = new SubscriptionPlansTable($analysis['plans_id']); + $subscription_name = $sp->getName() ?? 'Subscription'; + + // Proceed with creating new subscription + $subscriptionMetadata = [ + 'users_id' => (int)$analysis['users_id'], + 'plans_id' => (int)$analysis['plans_id'], + 'subscription_name' => $subscription_name, + 'initial_payment_id' => $parsed['transactionId'] + ]; + + $interval = (int)($sp->getHow_many_days() ?? 30); + $intervalUnit = 'days'; + + $subscriptionResult = AuthorizeNet::createSubscription( + $analysis['users_id'], + $analysis['amount'], + $subscriptionMetadata, + $interval, + $intervalUnit + ); + + Subscription::renew($analysis['users_id'], $analysis['plans_id'], SubscriptionTable::$gatway_authorize, $subscriptionResult['subscriptionId'], $subscriptionResult); + + if (!empty($subscriptionResult['error'])) { + _error_log('[Authorize.Net webhook] Subscription creation failed: ' . $subscriptionResult['msg']); + // Don't fail the entire webhook - the payment was processed successfully + } else { + _error_log('[Authorize.Net webhook] Subscription created: ' . $subscriptionResult['subscriptionId']); + } + } +} + +// 9) Return success response +echo json_encode([ + 'success' => true, + 'users_id' => $analysis['users_id'], + 'subscription' => $analysis['isASubscription'], + 'subscriptionId' => $analysis['subscriptionId'] ?? ($subscriptionResult['subscriptionId'] ?? null), + 'newSubscription' => $shouldCreateSubscription, + 'subscriptionCreated' => !empty($subscriptionResult) && empty($subscriptionResult['error']), + 'sigValid' => $parsed['signatureValid'], + 'logId' => $result['logId'] ?? null +]); diff --git a/plugin/YPTWallet/plugins/YPTWalletAuthorizeNet/YPTWalletAuthorizeNet.php b/plugin/YPTWallet/plugins/YPTWalletAuthorizeNet/YPTWalletAuthorizeNet.php new file mode 100644 index 0000000000..1daed43f81 --- /dev/null +++ b/plugin/YPTWallet/plugins/YPTWalletAuthorizeNet/YPTWalletAuthorizeNet.php @@ -0,0 +1,38 @@ + +
+ + +
+ + diff --git a/plugin/YPTWallet/plugins/YPTWalletAuthorizeNet/confirmRecurrentButton.php b/plugin/YPTWallet/plugins/YPTWalletAuthorizeNet/confirmRecurrentButton.php new file mode 100644 index 0000000000..8afafd52aa --- /dev/null +++ b/plugin/YPTWallet/plugins/YPTWalletAuthorizeNet/confirmRecurrentButton.php @@ -0,0 +1,82 @@ + + +
+ + +
+ +