1
0
Fork 0
mirror of https://github.com/DanielnetoDotCom/YouPHPTube synced 2025-10-03 01:39:24 +02:00
Oinktube/plugin/AuthorizeNet/AuthorizeNet.php

1592 lines
63 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
/**
* Authorize.Net plugin for AVideo
* - Customer Profiles & Payments: Official SDK (net\authorize\api\*)
* - Webhooks management: REST (file_get_contents) because the SDK does NOT expose webhook endpoints
*/
global $global;
require_once $global['systemRootPath'] . 'plugin/Plugin.abstract.php';
require_once $global['systemRootPath'] . 'plugin/AuthorizeNet/Objects/Anet_webhook_log.php';
use net\authorize\api\contract\v1\GetCustomerProfileResponse;
use net\authorize\api\constants\ANetEnvironment;
use net\authorize\api\contract\v1 as AnetAPI;
use net\authorize\api\controller as AnetController;
use net\authorize\api\contract\v1\CreateCustomerProfileResponse;
use net\authorize\api\contract\v1\GetHostedPaymentPageResponse;
use net\authorize\api\contract\v1\GetHostedProfilePageResponse;
use net\authorize\api\contract\v1\ARBSubscriptionType;
use net\authorize\api\contract\v1\PaymentScheduleType;
use net\authorize\api\contract\v1\PaymentScheduleType\IntervalAType;
use net\authorize\api\contract\v1\CustomerProfileIdType;
use net\authorize\api\contract\v1\ARBCreateSubscriptionRequest;
use net\authorize\api\contract\v1\ARBCreateSubscriptionResponse;
use net\authorize\api\contract\v1\ARBGetSubscriptionStatusRequest;
use net\authorize\api\contract\v1\ARBGetSubscriptionStatusResponse;
use net\authorize\api\controller\ARBCreateSubscriptionController;
use net\authorize\api\controller\ARBGetSubscriptionStatusController;
use net\authorize\api\contract\v1\OrderType;
use net\authorize\api\contract\v1\ARBGetSubscriptionRequest;
use net\authorize\api\contract\v1\ARBGetSubscriptionResponse;
use net\authorize\api\controller\ARBGetSubscriptionController;
class AuthorizeNet extends PluginAbstract
{
// --- inside AuthorizeNet class ---
/**
* Validate ANet webhook signature (HMAC-SHA512).
*
* @param string $rawBody
* @param array $headers
* @param string $signatureKeyHex
* @return array{valid:bool,expected:string,received:string}
*/
public static function verifySignature(string $rawBody, array $headers, string $signatureKeyHex): array
{
$received = $headers['X-ANET-Signature'] ?? ($headers['x-anet-signature'] ?? '');
if (empty($signatureKeyHex) || !ctype_xdigit($signatureKeyHex) || empty($received)) {
return ['valid' => 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'] ?? 0,
'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 '<button onclick="avideoModalIframeLarge(webSiteRootURL+\'plugin/AuthorizeNet/View/editor.php\')" class="btn btn-primary btn-xs btn-block"><i class="fa fa-edit"></i> Edit</button>';
}
public function getEmptyDataObject()
{
$obj = new stdClass();
$obj->apiLoginId = "";
$obj->transactionKey = "";
$obj->signatureKey = "";
$obj->sandbox = true;
$obj->subscriptionButtonLabel = "Subscribe With Credit Card";
$obj->paymentButtonLabel = "Pay With Credit Card";
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()];
}
}
}