388 lines
8.6 KiB
PHP
388 lines
8.6 KiB
PHP
<?php
|
|
/**
|
|
* @brief Billing Agreement Model
|
|
* @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
|
|
* @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\Customer;
|
|
|
|
/* 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;
|
|
}
|
|
|
|
/**
|
|
* Billing Agreement Model
|
|
*/
|
|
class _BillingAgreement extends \IPS\Patterns\ActiveRecord
|
|
{
|
|
/**
|
|
* @brief Billing Agreement is active and will charge automatically
|
|
*/
|
|
const STATUS_ACTIVE = 'active';
|
|
|
|
/**
|
|
* @brief Billing Agreement has been suspended but can be reactivated
|
|
*/
|
|
const STATUS_SUSPENDED = 'suspended';
|
|
|
|
/**
|
|
* @brief Billing Agreement has been canceled
|
|
*/
|
|
const STATUS_CANCELED = 'canceled';
|
|
|
|
/**
|
|
* @brief Multiton Store
|
|
*/
|
|
protected static $multitons;
|
|
|
|
/**
|
|
* @brief Database Table
|
|
*/
|
|
public static $databaseTable = 'nexus_billing_agreements';
|
|
|
|
/**
|
|
* @brief Database Prefix
|
|
*/
|
|
public static $databasePrefix = 'ba_';
|
|
|
|
/**
|
|
* Construct ActiveRecord from database row
|
|
*
|
|
* @param array $data Row from database table
|
|
* @param bool $updateMultitonStoreIfExists Replace current object in multiton store if it already exists there?
|
|
* @return static
|
|
*/
|
|
public static function constructFromData( $data, $updateMultitonStoreIfExists = TRUE )
|
|
{
|
|
$gateway = \IPS\nexus\Gateway::load( $data['ba_method'] );
|
|
$classname = 'IPS\nexus\Gateway\\' . $gateway->gateway . '\\BillingAgreement';
|
|
|
|
/* Initiate an object */
|
|
$obj = new $classname;
|
|
$obj->_new = FALSE;
|
|
|
|
/* Import data */
|
|
foreach ( $data as $k => $v )
|
|
{
|
|
if( static::$databasePrefix AND mb_strpos( $k, static::$databasePrefix ) === 0 )
|
|
{
|
|
$k = \substr( $k, \strlen( static::$databasePrefix ) );
|
|
}
|
|
|
|
$obj->_data[ $k ] = $v;
|
|
}
|
|
$obj->changed = array();
|
|
|
|
/* Init */
|
|
if ( method_exists( $obj, 'init' ) )
|
|
{
|
|
$obj->init();
|
|
}
|
|
|
|
/* Return */
|
|
return $obj;
|
|
}
|
|
|
|
/**
|
|
* Load and check permissions
|
|
*
|
|
* @param mixed $id ID to load
|
|
* @return \IPS\Content\Item
|
|
* @throws \OutOfRangeException
|
|
*/
|
|
public static function loadAndCheckPerms( $id )
|
|
{
|
|
$obj = static::load( $id );
|
|
|
|
if ( !$obj->canView() )
|
|
{
|
|
throw new \OutOfRangeException;
|
|
}
|
|
|
|
return $obj;
|
|
}
|
|
|
|
/**
|
|
* Member can view?
|
|
*
|
|
* @param \IPS\Member|NULL $member The member to check for, or NULL for currently logged in member
|
|
* @return bool
|
|
*/
|
|
public function canView( \IPS\Member $member = NULL )
|
|
{
|
|
$member = $member ?: \IPS\Member::loggedIn();
|
|
|
|
return $this->member->member_id === $member->member_id or array_key_exists( $member->member_id, iterator_to_array( $this->member->alternativeContacts( array( 'billing=1' ) ) ) );
|
|
}
|
|
|
|
/**
|
|
* Get member
|
|
*
|
|
* @return \IPS\Member
|
|
*/
|
|
public function get_member()
|
|
{
|
|
return \IPS\nexus\Customer::load( $this->_data['member'] );
|
|
}
|
|
|
|
/**
|
|
* Set member
|
|
*
|
|
* @param \IPS\Member $member Member
|
|
* @return void
|
|
*/
|
|
public function set_member( \IPS\Member $member )
|
|
{
|
|
$this->_data['member'] = $member->member_id;
|
|
}
|
|
|
|
/**
|
|
* Get payment gateway
|
|
*
|
|
* @return \IPS\nexus\Gateway
|
|
*/
|
|
public function get_method()
|
|
{
|
|
return \IPS\nexus\Gateway::load( $this->_data['method'] );
|
|
}
|
|
|
|
/**
|
|
* Set payment gateway
|
|
*
|
|
* @param \IPS\nexus\Gateway $gateway Payment gateway
|
|
* @return void
|
|
*/
|
|
public function set_method( \IPS\nexus\Gateway $gateway )
|
|
{
|
|
$this->_data['method'] = $gateway->id;
|
|
}
|
|
|
|
/**
|
|
* Get start date
|
|
*
|
|
* @return \IPS\DateTime
|
|
*/
|
|
public function get_started()
|
|
{
|
|
return \IPS\DateTime::ts( $this->_data['started'] );
|
|
}
|
|
|
|
/**
|
|
* Set next payment date
|
|
*
|
|
* @param \IPS\DateTime $date The invoice date
|
|
* @return void
|
|
*/
|
|
public function set_next_cycle( \IPS\DateTime $date = NULL )
|
|
{
|
|
$this->_data['next_cycle'] = $date ? $date->getTimestamp() : NULL;
|
|
}
|
|
|
|
/**
|
|
* Get next payment date
|
|
*
|
|
* @return \IPS\DateTime
|
|
*/
|
|
public function get_next_cycle()
|
|
{
|
|
return $this->_data['next_cycle'] ? \IPS\DateTime::ts( $this->_data['next_cycle'] ) : NULL;
|
|
}
|
|
|
|
/**
|
|
* Set start date
|
|
*
|
|
* @param \IPS\DateTime $date The invoice date
|
|
* @return void
|
|
*/
|
|
public function set_started( \IPS\DateTime $date )
|
|
{
|
|
$this->_data['started'] = $date->getTimestamp();
|
|
}
|
|
|
|
/**
|
|
* Suspend
|
|
*
|
|
* @return void
|
|
* @throws \DomainException
|
|
*/
|
|
public function refresh()
|
|
{
|
|
try
|
|
{
|
|
while( true )
|
|
{
|
|
$this->checkForLatestTransaction();
|
|
}
|
|
}
|
|
catch( \OutOfRangeException | \UnderflowException $e )
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Suspend
|
|
*
|
|
* @return void
|
|
* @throws \DomainException
|
|
*/
|
|
public function suspend()
|
|
{
|
|
$this->doSuspend();
|
|
|
|
$this->member->log( 'billingagreement', array( 'type' => 'suspend', 'id' => $this->id, 'gw_id' => $this->gw_id ) );
|
|
$this->next_cycle = NULL;
|
|
$this->save();
|
|
}
|
|
|
|
/**
|
|
* Reactivate
|
|
*
|
|
* @return void
|
|
* @throws \DomainException
|
|
*/
|
|
public function reactivate()
|
|
{
|
|
$this->doReactivate();
|
|
|
|
$this->member->log( 'billingagreement', array( 'type' => 'reactivate', 'id' => $this->id, 'gw_id' => $this->gw_id ) );
|
|
$this->next_cycle = $this->nextPaymentDate();
|
|
$this->save();
|
|
}
|
|
|
|
/**
|
|
* Cancel
|
|
*
|
|
* @return void
|
|
* @throws \DomainException
|
|
*/
|
|
public function cancel()
|
|
{
|
|
$this->doCancel();
|
|
|
|
$this->member->log( 'billingagreement', array( 'type' => 'cancel', 'id' => $this->id, 'gw_id' => $this->gw_id ) );
|
|
$this->next_cycle = NULL;
|
|
$this->canceled = TRUE;
|
|
$this->save();
|
|
}
|
|
|
|
/**
|
|
* Front-End URL
|
|
*
|
|
* @return \IPS\Http\Url
|
|
*/
|
|
public function url()
|
|
{
|
|
return \IPS\Http\Url::internal( "app=nexus&module=clients&controller=billingagreements&do=view&id={$this->id}", 'front', 'clientsbillingagreement' );
|
|
}
|
|
|
|
/**
|
|
* ACP URL
|
|
*
|
|
* @return \IPS\Http\Url
|
|
*/
|
|
public function acpUrl()
|
|
{
|
|
return \IPS\Http\Url::internal( "app=nexus&module=payments&controller=billingagreements&id={$this->id}", 'admin' );
|
|
}
|
|
|
|
/**
|
|
* Check for latest unclaimed transactions
|
|
*
|
|
* @return void
|
|
*/
|
|
public function checkForLatestTransaction()
|
|
{
|
|
/* Fetch the latest unclaimed transaction */
|
|
try
|
|
{
|
|
$transaction = $this->latestUnclaimedTransaction();
|
|
}
|
|
/* If there isn't one yet, it might just be that PayPal hasn't taken it yet. Let it try for up to 5 days before giving up, which is how long PayPal will try for if there's an issue. */
|
|
catch ( \OutOfRangeException $e )
|
|
{
|
|
if ( $this->next_cycle->getTimestamp() > ( time() - ( 86400 * 5 ) ) )
|
|
{
|
|
throw new \UnderflowException;
|
|
}
|
|
else
|
|
{
|
|
/* We are here because we could not find any unclaimed transactions and it has been at least 5 days since we expected to. Verify the next cycle before proceeding because it may not have been set properly in the past so before we say there's a problem let's check. */
|
|
$nextPaymentDate = $this->nextPaymentDate();
|
|
|
|
if( $nextPaymentDate->getTimestamp() > time() )
|
|
{
|
|
$this->next_cycle = $nextPaymentDate;
|
|
$this->save();
|
|
|
|
throw new \UnderflowException;
|
|
}
|
|
|
|
throw $e;
|
|
}
|
|
}
|
|
|
|
/* Get purchases */
|
|
$purchases = new \IPS\Patterns\ActiveRecordIterator( \IPS\Db::i()->select( '*', 'nexus_purchases', array( 'ps_billing_agreement=?', $this->id ) ), 'IPS\nexus\Purchase' );
|
|
|
|
/* Generate an invoice */
|
|
$invoice = new \IPS\nexus\Invoice;
|
|
$invoice->system = TRUE;
|
|
$invoice->currency = $transaction->amount->currency;
|
|
$invoice->member = $this->member;
|
|
$invoice->billaddress = $this->member->primaryBillingAddress();
|
|
foreach ( $purchases as $purchase )
|
|
{
|
|
$invoice->addItem( \IPS\nexus\Invoice\Item\Renewal::create( $purchase ) );
|
|
}
|
|
$invoice->save();
|
|
|
|
/* Assign the transaction to it */
|
|
$transaction->invoice = $invoice;
|
|
$transaction->save();
|
|
$transaction->approve();
|
|
$invoice->status = $transaction->invoice->status;
|
|
|
|
/* Log */
|
|
$invoice->member->log( 'transaction', array(
|
|
'type' => 'paid',
|
|
'status' => \IPS\nexus\Transaction::STATUS_PAID,
|
|
'id' => $transaction->id,
|
|
'invoice_id' => $invoice->id,
|
|
'invoice_title' => $invoice->title,
|
|
'automatic' => TRUE,
|
|
), FALSE );
|
|
|
|
/* Update the purchase */
|
|
if ( $invoice->status !== $invoice::STATUS_PAID )
|
|
{
|
|
foreach ( $purchases as $purchase )
|
|
{
|
|
$purchase->invoice_pending = $invoice;
|
|
$purchase->save();
|
|
}
|
|
}
|
|
|
|
/* Send notification */
|
|
$invoice->sendNotification();
|
|
|
|
/* Update billing agreement */
|
|
if ( $invoice->status === $invoice::STATUS_PAID )
|
|
{
|
|
$this->next_cycle = $this->nextPaymentDate();
|
|
}
|
|
else
|
|
{
|
|
$this->next_cycle = NULL;
|
|
}
|
|
$this->save();
|
|
}
|
|
} |