Invision Power Services, Inc. * @copyright (c) Invision Power Services, Inc. * @license https://www.invisioncommunity.com/legal/standards/ * @package Invision Community * @subpackage Nexus * @since 16 Dec 2015 */ namespace IPS\nexus\Gateway\PayPal; /* To prevent PHP errors (extending class does not exist) revealing path */ if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) ) { header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' ); exit; } /** * PayPal Billing Agreement */ class _BillingAgreement extends \IPS\nexus\Customer\BillingAgreement { /** * Get status * * @return string See STATUS_* constants * @throws \IPS\nexus\Gateway\PayPal\Exception */ public function status() { $data = $this->_getData(); switch ( mb_strtoupper( $data['status'] ?? $data['state'] ) ) { case 'ACTIVE': case 'PENDING': case 'APPROVAL_PENDING': case 'APPROVED': case 'REACTIVATE': return static::STATUS_ACTIVE; break; case 'SUSPEND': case 'SUSPENDED': return static::STATUS_SUSPENDED; break; case 'EXPIRED': case 'CANCEL': case 'CANCELLED': return static::STATUS_CANCELED; break; } } /** * Get term * * @return \IPS\nexus\Purchase\RenewalTerm * @throws \IPS\nexus\Gateway\PayPal\Exception */ public function term() { $data = $this->_getData(); if ( isset( $data['plan_id'] ) ) { $plan = $this->method->api( "billing/plans/{$data['plan_id']}", NULL, 'get' ); $cycle = array_pop( $plan['billing_cycles'] ); return new \IPS\nexus\Purchase\RenewalTerm( new \IPS\nexus\Money( $cycle['pricing_scheme']['fixed_price']['value'], $cycle['pricing_scheme']['fixed_price']['currency_code'] ), new \DateInterval( 'P' . $cycle['frequency']['interval_count'] . mb_substr( $cycle['frequency']['interval_unit'], 0, 1 ) ) ); } else { $amount = $data['plan']['payment_definitions'][0]['amount']['value']; if ( isset( $data['plan']['payment_definitions'][0]['charge_models'][0] ) ) { $amount += $data['plan']['payment_definitions'][0]['charge_models'][0]['amount']['value']; } return new \IPS\nexus\Purchase\RenewalTerm( new \IPS\nexus\Money( $amount, $data['plan']['payment_definitions'][0]['amount']['currency'] ), new \DateInterval( 'P' . $data['plan']['payment_definitions'][0]['frequency_interval'] . mb_substr( $data['plan']['payment_definitions'][0]['frequency'], 0, 1 ) ) ); } } /** * Get next payment date * * @return \IPS\DateTime * @throws \IPS\nexus\Gateway\PayPal\Exception */ public function nextPaymentDate() { $data = $this->_getData(); if ( isset( $data['billing_info'] ) ) { return new \IPS\DateTime( $data['billing_info']['next_billing_time'] ); } else { return new \IPS\DateTime( $data['agreement_details']['next_billing_date'] ); } } /** * Get latest unclaimed transaction (only gets transactions within the last 31 days which do not yet have a matching transaction) * * @return \IPS\nexus\Transaction * @throws \IPS\nexus\Gateway\PayPal\Exception * @throws \OutOfRangeException * @throws \IPS\Http\Url\Exception */ public function latestUnclaimedTransaction() { $data = $this->_getData(); /* PayPal Subscriptions */ if ( isset( $data['plan_id'] ) ) { $url = ''; $transactions = $this->method->api( "billing/subscriptions/{$this->gw_id}/transactions?start_time=" . \IPS\DateTime::create()->sub( new \DateInterval( 'P31D' ) )->rfc3339() . '&end_time=' . \IPS\DateTime::create()->rfc3339(), NULL, 'get' ); foreach ( array_reverse( $transactions['transactions'] ?? [] ) as $t ) { if ( $t['status'] == 'COMPLETED' ) { try { /* @note SELECT_FROM_WRITE_SERVER transaction race condition */ \IPS\Db::i()->select( 't_id', 'nexus_transactions', [ 't_gw_id=? AND t_method=?', $t['id'], $this->method->id ], flags: \IPS\Db::SELECT_FROM_WRITE_SERVER )->first(); } catch ( \UnderflowException $e ) { $transaction = new \IPS\nexus\Transaction; $transaction->member = $this->member; $transaction->method = $this->method; $transaction->amount = new \IPS\nexus\Money( $t['amount_with_breakdown']['gross_amount']['value'], $t['amount_with_breakdown']['gross_amount']['currency_code'] ); $transaction->date = new \IPS\DateTime( $t['time'] ); $transaction->extra = array( 'automatic' => TRUE ); $transaction->gw_id = $t['id']; $transaction->billing_agreement = $this; return $transaction; } } } } /* Legacy Billing Agreement */ else { $url = "payments/billing-agreements/{$this->gw_id}/transactions?start_date=" . date( 'Y-m-d', time() - ( 86400 * 31 ) ) . '&end_date=' . date( 'Y-m-d' ); $transactions = $this->method->api( $url, NULL, 'get' ); foreach ( array_reverse( $transactions['agreement_transaction_list'] ) as $t ) { if ( $t['status'] == 'Completed' ) { try { /* @note SELECT_FROM_WRITE_SERVER transaction race condition */ \IPS\Db::i()->select( 't_id', 'nexus_transactions', [ 't_gw_id=? AND t_method=?', $t['transaction_id'], $this->method->id ], flags: \IPS\Db::SELECT_FROM_WRITE_SERVER )->first(); } catch ( \UnderflowException $e ) { $transaction = new \IPS\nexus\Transaction; $transaction->member = $this->member; $transaction->method = $this->method; $transaction->amount = new \IPS\nexus\Money( $t['amount']['value'], $t['amount']['currency'] ); $transaction->date = new \IPS\DateTime( $t['time_stamp'] ); $transaction->extra = array( 'automatic' => TRUE ); $transaction->gw_id = $t['transaction_id']; $transaction->billing_agreement = $this; return $transaction; } } } } throw new \OutOfRangeException( "{$this->id} ({$this->gw_id})\n{$url}\n\n" . print_r( $transactions, TRUE ) ); } /** * Suspend * * @return void * @throws \DomainException */ public function doSuspend() { $data = $this->_getData(); if ( isset( $data['plan_id'] ) ) { $this->method->api( "billing/subscriptions/{$this->gw_id}/suspend", array( 'reason' => 'Suspend' ) ); } else { $this->method->api( "payments/billing-agreements/{$this->gw_id}/suspend", array( 'note' => 'Suspend' ) ); } } /** * Reactivate * * @return void * @throws \DomainException */ public function doReactivate() { $data = $this->_getData(); if ( isset( $data['plan_id'] ) ) { $this->method->api( "billing/subscriptions/{$this->gw_id}/activate", array( 'reason' => 'Reactivate' ) ); } else { $this->method->api( "payments/billing-agreements/{$this->gw_id}/re-activate", array( 'note' => 'Reactivate' ) ); } } /** * Cancel * * @return void * @throws \DomainException */ public function doCancel() { $data = $this->_getData(); if ( isset( $data['plan_id'] ) ) { $this->method->api( "billing/subscriptions/{$this->gw_id}/cancel", array( 'reason' => 'Cancel' ) ); } else { $this->method->api( "payments/billing-agreements/{$this->gw_id}/cancel", array( 'note' => 'Cancel' ) ); } } /** * @brief Cached data */ protected $_payPalData = NULL; /** * Get data * * @return array * @throws \IPS\nexus\Gateway\PayPal\Exception */ public function _getData() { if ( $this->_payPalData === NULL ) { try { $this->_payPalData = $this->method->api( "billing/subscriptions/{$this->gw_id}", NULL, 'get' ); if( !isset( $this->_payPalData['plan_id'] ) ) { throw new \RuntimeException; } } catch ( \Exception $e ) { $this->_payPalData = $this->method->api( "payments/billing-agreements/{$this->gw_id}", NULL, 'get' ); } } return $this->_payPalData; } }