Invision Power Services, Inc. * @copyright (c) Invision Power Services, Inc. * @license https://www.invisioncommunity.com/legal/standards/ * @package Invision Community * @subpackage Nexus * @since 10 Feb 2014 */ namespace IPS\nexus; /* 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; } /** * Invoice Model */ class _Invoice extends \IPS\Patterns\ActiveRecord { const STATUS_PAID = 'paid'; const STATUS_PENDING = 'pend'; const STATUS_EXPIRED = 'expd'; const STATUS_CANCELED = 'canc'; /** * @brief Multiton Store */ protected static $multitons; /** * @brief Database Table */ public static $databaseTable = 'nexus_invoices'; /** * @brief Database Prefix */ public static $databasePrefix = 'i_'; /** * Load and check permissions * * @return \IPS\Content\Item * @throws \OutOfRangeException */ public static function loadAndCheckPerms( $id ) { $obj = static::load( $id ); if ( !$obj->canView() ) { throw new \OutOfRangeException; } return $obj; } /** * Get statuses * * @return array */ public static function statuses() { $options = array(); $reflection = new \ReflectionClass( get_called_class() ); foreach ( $reflection->getConstants() as $k => $v ) { if ( mb_substr( $k, 0, 7 ) === 'STATUS_' ) { $options[ $v ] = "istatus_{$v}"; } } return $options; } /** * Get invoices table * * @param array $where Where clause * @param string $ref Referer * @return \IPS\Helpers\Table\Db */ public static function table( $where = array(), \IPS\Http\Url $url, $ref = 't' ) { /* Create the table */ $table = new \IPS\Helpers\Table\Db( 'nexus_invoices', $url, $where ); $table->sortBy = $table->sortBy ?: 'i_date'; /* Format Columns */ $table->include = array( 'i_status', 'i_id', 'i_title', 'i_member', 'i_total', 'i_date' ); $table->parsers = array( 'i_status' => function( $val ) { return \IPS\Theme::i()->getTemplate('invoices')->status( $val ); }, 'i_member' => function ( $val, $row ) { if ( $val ) { return \IPS\Theme::i()->getTemplate('global')->userLink( \IPS\Member::load( $val ) ); } elseif ( $row['i_guest_data'] ) { $data = json_decode( $row['i_guest_data'], TRUE ); return \IPS\nexus\Customer::constructFromData( $data['member'] )->cm_name; } else { return \IPS\Theme::i()->getTemplate('global')->userLink( \IPS\Member::load( 0 ) ); } }, 'i_total' => function( $val, $row ) { return (string) new \IPS\nexus\Money( $val, $row['i_currency'] ); }, 'i_date' => function( $val ) { return \IPS\DateTime::ts( $val ); } ); /* Buttons */ $table->rowButtons = function( $row ) use( $ref ) { return array_merge( array( 'view' => array( 'icon' => 'search', 'title' => 'invoice_view', 'link' => \IPS\Http\Url::internal( "app=nexus&module=payments&controller=invoices&do=view&id={$row['i_id']}" ) ), ), \IPS\nexus\Invoice::constructFromData( $row )->buttons( $ref ) ); }; return $table; } /** * @brief Being generated by system? */ public $system = FALSE; /** * Set Default Values * * @return void */ public function setDefaultValues() { $this->status = static::STATUS_PENDING; $this->items = json_encode( array() ); $this->date = new \IPS\DateTime; $this->renewal_ids = array(); $this->guest_data = array(); $this->_data['total'] = 0; $this->_data['member'] = 0; $this->status_extra = array(); } /** * 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' ) ) ) ); } /** * Set title * * @param string * @return void * @throws \LengthException */ public function set_title( $title ) { if ( mb_strlen( $title ) > 128 ) { throw new \LengthException; } $this->_data['title'] = $title; } /** * @brief Member */ protected $_member; /** * Get member * * @return \IPS\nexus\Customer */ public function get_member() { if ( $this->_member === NULL ) { try { $this->_member = \IPS\nexus\Customer::load( $this->_data['member'] ); /* \IPS\nexus\Customer does not necessarily throw an OutOfRangeException if passed a value of 0 (a guest instance is valid) - throw one here so we can bubble up to get the correct information for guests. */ if ( !$this->_member->member_id ) { throw new \OutOfRangeException; } } catch ( \OutOfRangeException $e ) { $this->_member = new \IPS\nexus\Customer; if ( $this->_data['member'] ) { $this->_member->_data['cm_first_name'] = \IPS\Member::loggedIn()->language()->addToStack('deleted_member'); } elseif ( $this->guest_data ) { foreach ( $this->guest_data['member'] as $k => $v ) { $this->_member->_data[ $k ] = $v; $this->_member->changed[ $k ] = $v; } } } } return $this->_member; } /** * Set member * * @param \IPS\Member * @return void */ public function set_member( \IPS\Member $member ) { $this->_data['member'] = (int) $member->member_id; $this->_member = NULL; if ( !$this->currency and $member instanceof \IPS\nexus\Customer ) { $this->currency = $member->defaultCurrency(); } } /** * @brief Items iterator */ protected $_itemsIterator; /** * Get items * * @return array */ public function get_items() { if ( $this->_itemsIterator === NULL ) { /* Decode */ $itemsJson = json_decode( $this->_data['items'], TRUE ) ?: array(); /* Check we actually have a valid array with items (very old invoices from IP.Subscriptions may not have anything) and just create a dummy if so */ $itemsJson = array_filter( $itemsJson, function( $val ) { return is_array( $val ) and isset( $val['act'] ); } ); if ( !$itemsJson and $this->total->amount->isGreaterThanZero() ) { $itemsJson = array( array( 'act' => 'charge', 'app' => 'nexus', 'type' => 'charge', 'cost' => $this->total->amountAsString(), 'tax' => NULL, 'quantity' => 1, 'itemName' => $this->title, 'itemID' => NULL, 'physical' => FALSE, 'methods' => '*', 'cfields' => array(), 'extra' => NULL, ) ); } /* Return it */ $this->_itemsIterator = new \IPS\nexus\Invoice\ItemsIterator( $itemsJson ); $this->_itemsIterator->currency = $this->currency; } return $this->_itemsIterator; } /** * Get total * * @return \IPS\nexus\Money */ public function get_total() { return new \IPS\nexus\Money( $this->_data['total'], $this->currency ); } /** * Set total * * @param \IPS\nexus\Money $total The total * @return void */ public function set_total( \IPS\nexus\Money $total ) { $this->_data['total'] = $total->amount; } /** * Get date * * @return \IPS\DateTime */ public function get_date() { return \IPS\DateTime::ts( $this->_data['date'] ); } /** * Set date * * @param \IPS\DateTime $date The invoice date * @return void */ public function set_date( \IPS\DateTime $date ) { $this->_data['date'] = $date->getTimestamp(); } /** * Get paid date date * * @return \IPS\DateTime|NULL */ public function get_paid() { return $this->_data['paid'] ? \IPS\DateTime::ts( $this->_data['paid'] ) : NULL; } /** * Set paid date * * @param \IPS\DateTime $date The invoice date * @return void */ public function set_paid( \IPS\DateTime $date ) { $this->_data['paid'] = $date->getTimestamp(); } /** * Get return URI * * @return \IPS\Http\Url|NULL */ public function get_return_uri() { return $this->_data['return_uri'] ? \IPS\Http\Url::internal( $this->_data['return_uri'] ) : NULL; } /** * Get status information * * @return mixed */ public function get_status_extra() { return json_decode( $this->_data['status_extra'], TRUE ); } /** * Set status information * * @param mixed $extra The data * @return void */ public function set_status_extra( $extra ) { $this->_data['status_extra'] = json_encode( $extra ); } /** * Get renewal IDs * * @return array */ public function get_renewal_ids() { return explode( ',', $this->_data['renewal_ids'] ); } /** * Set renewal IDs * * @param array $ids The IDs * @return void */ public function set_renewal_ids( $ids ) { array_unique( $ids ); $ids = array_filter( $ids ); $this->_data['renewal_ids'] = implode( ',', $ids ); } /** * Get shipping address * * @return \IPS\GeoLocation|NULL */ public function get_shipaddress() { if ( !$this->hasPhysicalItems() ) { return NULL; } if ( isset( $this->_data['shipaddress'] ) and $this->_data['shipaddress'] ) { return \IPS\GeoLocation::buildFromJson( $this->_data['shipaddress'] ); } else { try { return \IPS\GeoLocation::buildFromJson( \IPS\Db::i()->select( 'address', 'nexus_customer_addresses', array( 'member=? AND primary_shipping=1', $this->member->member_id ) )->first() ); } catch ( \UnderflowException $e ) { return NULL; } } } /** * Set shipping address * * @param \IPS\GeoLocation $shippingAddress The shipping address * @return void */ public function set_shipaddress( \IPS\GeoLocation $shippingAddress ) { $this->_data['shipaddress'] = json_encode( $shippingAddress ); } /** * Get billing address * * @return \IPS\GeoLocation|NULL */ public function get_billaddress() { if ( isset( $this->_data['billaddress'] ) and $this->_data['billaddress'] ) { return \IPS\GeoLocation::buildFromJson( $this->_data['billaddress'] ); } else { return NULL; } } /** * Set billing address * * @param \IPS\GeoLocation $billingAddress The billing address * @return void */ public function set_billaddress( \IPS\GeoLocation $billingAddress ) { $this->_data['billaddress'] = json_encode( $billingAddress ); $this->_data['billcountry'] = $billingAddress->country; } /** * Get guest data * * @return mixed */ public function get_guest_data() { return $this->_data['guest_data'] ? json_decode( $this->_data['guest_data'], TRUE ) : NULL; } /** * Set guest data * * @param mixed $data The data * @return void */ public function set_guest_data( $data ) { $this->_data['guest_data'] = $data ? json_encode( $data ) : NULL; } /** * Set a title automatically based on item names * * @return void */ public function setDefaultTitle() { $titles = array(); foreach ( $this->items as $item ) { $titles[] = ( $item->name . ( $item->quantity > 1 ? " x{$item->quantity}" : '' ) ); } $thisTitle = implode( ', ', $titles ); if ( mb_strlen( $thisTitle ) > 128 ) { $thisTitle = mb_substr( $thisTitle, 0, 125 ) . '...'; } $this->title = $thisTitle; } /** * Save Changed Columns * * @return void */ public function save() { if ( !$this->title ) { $this->setDefaultTitle(); } if ( $this->_new ) { parent::save(); $this->member->log( 'invoice', array( 'id' => $this->id, 'title' => $this->title, 'status' => $this->status, 'system' => $this->system ), $this->system ? FALSE : NULL ); } else { parent::save(); } } /** * Delete * * @return void */ public function delete() { \IPS\File::unclaimAttachments( 'nexus_Purchases', NULL, NULL, "invoice-{$this->id}" ); parent::delete(); foreach ( $this->transactions( array( \IPS\nexus\Transaction::STATUS_PENDING, \IPS\nexus\Transaction::STATUS_WAITING, \IPS\nexus\Transaction::STATUS_HELD, \IPS\nexus\Transaction::STATUS_REVIEW, \IPS\nexus\Transaction::STATUS_GATEWAY_PENDING ) ) as $transaction ) { if ( $transaction->status === \IPS\nexus\Transaction::STATUS_PENDING ) { $transaction->delete(); } else { try { $transaction->method->void( $transaction ); } catch ( \Exception $e ){} $extra = $transaction->extra; $extra['history'][] = array( 's' => \IPS\nexus\Transaction::STATUS_REFUSED, 'on' => time(), 'note' => 'invoiceDeleted' ); $transaction->extra = $extra; $transaction->status = \IPS\nexus\Transaction::STATUS_REFUSED; $transaction->save(); } } } /* !Items */ /** * Add item * * @param \IPS\nexus\Invoice\Item $item The item * @array int|NULL $cartKey Our cart key for this item, or NULL. Used to make sure items are properly associated with their parents if not bought yet. * @return int */ public function addItem( \IPS\nexus\Invoice\Item $item, $cartKey = NULL ) { /* Set basic data */ $data = array( 'act' => $item::$act, 'app' => isset( $item::$application ) ? $item::$application : $item->application, 'type' => isset( $item::$type ) ? $item::$type : $item->type, 'cost' => $item->price->amountAsString(), 'tax' => $item->tax ? $item->tax->id : NULL, 'quantity' => $item->quantity, 'itemName' => $item->name, 'itemID' => $item->id, 'physical' => FALSE, 'methods' => $item->paymentMethodIds ?: '*', 'cfields' => $item->details, 'extra' => $item->extra, ); /* Parent? */ if ( isset( $item->parent ) and $item->parent !== NULL ) { if ( is_int( $item->parent ) ) { $data['assoc'] = $item->parent; $data['assocBought'] = FALSE; } else { $data['assoc'] = $item->parent->id; $data['assocBought'] = TRUE; } $data['groupParent'] = $item->groupWithParent; } /* Renewal term? */ if ( isset( $item->renewalTerm ) ) { $renewTermData = $item->renewalTerm->getTerm(); $data['renew_term'] = $renewTermData['term']; $data['renew_units'] = $renewTermData['unit']; $data['renew_cost'] = $item->renewalTerm->cost->amountAsString(); if ( $item->renewalTerm->gracePeriod ) { $data['grace_period'] = \IPS\DateTime::create()->add( $item->renewalTerm->gracePeriod )->getTimestamp() - time(); } } /* Expire date? */ if ( isset( $item->expireDate ) ) { $data['expires'] = $item->expireDate->getTimestamp(); } /* Physical? */ if ( $item->physical ) { $data['physical'] = TRUE; $data['shipping'] = $item->shippingMethodIds ?: '*'; $data['weight'] = $item->weight->kilograms; $data['length'] = $item->length->metres; $data['width'] = $item->width->metres; $data['height'] = $item->height->metres; } /* Pay to someone else? */ if ( $item->payTo ) { $data['payTo'] = $item->payTo->member_id; if ( $item->commission ) { $data['commission'] = $item->commission; } if ( $item->fee ) { $data['fee'] = $item->fee->amount; } } /* Add it */ $items = json_decode( $this->_data['items'], TRUE ); if ( $cartKey !== NULL ) { $items[ $cartKey ] = $data; } else { $items[] = $data; } /* Get the ID */ $ids = array_keys( $items ); $id = array_pop( $ids ); /* Save it */ $this->items = json_encode( $items ); $this->_itemsIterator = NULL; /* Is it a renewal */ if ( $item instanceof \IPS\nexus\Invoice\Item\Renewal ) { $renewalIds = $this->renewal_ids; $renewalIds[] = $item->id; $this->renewal_ids = $renewalIds; } /* Increase the total */ $this->recalculateTotal(); /* Return the ID */ return $id; } /** * Change item * * @param int $index The index * @param array $data Data to set * @return void */ public function changeItem( $index, $data ) { $items = json_decode( $this->_data['items'], TRUE ); foreach ( $data as $k => $v ) { $items[ $index ][ $k ] = $v; } $this->items = json_encode( $items ); $this->_itemsIterator = NULL; } /** * Remove item * * @param int $index The index * @return void */ public function removeItem( $index ) { $items = json_decode( $this->_data['items'], TRUE ); unset( $items[ $index ] ); $this->items = json_encode( $items ); $this->_itemsIterator = NULL; $this->recalculateTotal(); } /** * Recalculate the total amount * * @return void */ public function recalculateTotal() { $summary = $this->summary(); $this->total = $summary['total']; } /** * Get summary for invoice tables and so on * * @return array */ public function summary() { $return = array(); $return['items'] = array(); $shippingItems = array(); $subtotal = new \IPS\Math\Number('0'); $taxableAmounts = array(); $taxClasses = \IPS\nexus\Tax::roots(); /* Reduce taxable amount from coupons */ $couponTaxReductions = array(); foreach ( $this->items as $itemId => $item ) { if ( $item instanceof \IPS\nexus\extensions\nexus\Item\CouponDiscount ) { if ( isset( $item->extra['type'] ) ) { if ( isset( $item->extra['items'] ) ) { $itemCount = count( $item->extra['items'] ); } else { $itemCount = 0; foreach ( clone $this->items as $k => $_item ) // We have to clone because $items is an iterator, and we're already looping them, but we need a separate loop { if ( $_item != $item and !( $_item instanceof \IPS\nexus\extensions\nexus\Item\ShippingCharge ) and !( $_item instanceof \IPS\nexus\extensions\nexus\Item\CouponDiscount ) ) { $itemCount++; } } } foreach ( clone $this->items as $k => $_item ) // We have to clone because $items is an iterator, and we're already looping them, but we need a separate loop { if ( $_item != $item and !( $_item instanceof \IPS\nexus\extensions\nexus\Item\CouponDiscount ) and !( $_item instanceof \IPS\nexus\extensions\nexus\Item\ShippingCharge ) and ( !isset( $item->extra['items'] ) or in_array( $k, $item->extra['items'] ) ) ) { if ( $item->extra['type'] == 'p' ) { $reductionAmount = $_item->linePrice()->amount->multiply( new \IPS\Math\Number("{$item->extra['value']}") )->divide( new \IPS\Math\Number('100') ); } else { $reductionAmount = ( new \IPS\Math\Number( "{$item->extra['value']}" ) )->divide( new \IPS\Math\Number( "{$itemCount}" ) )->multiply( new \IPS\Math\Number( '-1' ) ); } if ( isset( $couponTaxReductions[ $k ] ) ) { $couponTaxReductions[ $k ] = $couponTaxReductions[ $k ]->add( $reductionAmount ); } else { $couponTaxReductions[ $k ] = $reductionAmount; } } } } } } /* Work out the line totals */ foreach ( $this->items as $k => $item ) { if ( $item instanceof \IPS\nexus\extensions\nexus\Item\ShippingCharge ) { $shippingItems[ $k ] = $item; } else { $subtotal = $subtotal->add( $item->linePrice()->amount ); if ( isset( $item->tax ) ) { $taxAmount = $item->linePrice()->amount; if ( isset( $couponTaxReductions[ $k ] ) ) { $taxAmount = $taxAmount->subtract( $couponTaxReductions[ $k ] ); } if ( isset( $taxableAmounts[ $item->tax->id ] ) ) { $taxableAmounts[ $item->tax->id ] = $taxableAmounts[ $item->tax->id ]->add( $taxAmount ); } else { $taxableAmounts[ $item->tax->id ] = $taxAmount; } } $return['items'][ $k ] = $item; } } /* Subtotal (subtotal is caulculated BEFORE shipping is added) */ $return['subtotal'] = new \IPS\nexus\Money( $subtotal, $this->currency ); /* Add shipping */ $return['shipping'] = array(); $shipping = new \IPS\Math\Number('0'); foreach ( $shippingItems as $k => $item ) { $linePrice = $item->linePrice()->amount; $shipping = $shipping->add( $linePrice ); if ( isset( $item->tax ) ) { if ( isset( $taxableAmounts[ $item->tax->id ] ) ) { $taxableAmounts[ $item->tax->id ] = $taxableAmounts[ $item->tax->id ]->add( $linePrice ); } else { $taxableAmounts[ $item->tax->id ] = $linePrice; } } $return['shipping'][ $k ] = $item; } $return['shippingTotal'] = new \IPS\nexus\Money( $shipping, $this->currency ); /* Now work out the tax */ $return['tax'] = array(); $tax = new \IPS\Math\Number('0'); foreach ( $taxableAmounts as $taxId => $amount ) { $rate = $taxClasses[ $taxId ]->rate( $this->billaddress ); $amount = $amount->multiply( new \IPS\Math\Number( $rate ) ); $amount = new \IPS\nexus\Money( $amount, $this->currency ); $return['tax'][ $taxId ] = array( 'name' => $taxClasses[ $taxId ]->_title, 'rate' => $rate, 'amount' => $amount ); $tax = $tax->add( $amount->amount ); } $return['taxTotal'] = new \IPS\nexus\Money( $tax, $this->currency ); /* Add it all together */ $return['total'] = new \IPS\nexus\Money( $subtotal->add( $shipping )->add( $tax ), $this->currency ); return $return; } /* !Information */ /** * Does the invoice contain items that require a billing address? * * @return bool */ public function hasItemsRequiringBillingAddress() { foreach ( $this->items as $item ) { if ( $item->requiresBillingAddress() ) { return TRUE; } } return FALSE; } /** * Does the invoice contain physical items that need shipping? * * @return bool */ public function hasPhysicalItems() { foreach ( $this->items as $item ) { if ( $item->physical ) { return TRUE; } } return FALSE; } /** * Requires login? * * @return bool */ public function requiresLogin() { foreach ( $this->items as $item ) { if ( $item::$requiresAccount ) { return TRUE; } } return FALSE; } /** * Amounts that go to a member * * @return array */ public function payToRecipients() { $recipients = array(); foreach ( $this->items as $item ) { if ( $item->payTo ) { if ( !isset( $recipients[ $item->payTo->member_id ] ) ) { $recipients[ $item->payTo->member_id ] = new \IPS\Math\Number('0'); } $itemRecipientAmounts = $item->recipientAmounts(); $recipients[ $item->payTo->member_id ] = $recipients[ $item->payTo->member_id ]->add( $itemRecipientAmounts['recipient_final']->amount ); } } return $recipients; } /** * Get the commission information * * @param \IPS\Math\Number $amountReceived The amount the invoice is for, minus amounts being given to other recipients * @return array( 'rule' => \IPS\nexus\CommissionRule, 'referrer' => \IPS\nexus\Customer, 'amount' => \IPS\nexus\Money ) */ public function commission( \IPS\Math\Number $amountReceived ) { if ( !isset( $this->status_extra['commissionrule'] ) ) { try { /* Get the referrer */ $referrer = \IPS\Member::load( \IPS\Db::i()->select( 'referred_by', 'nexus_referrals', array( 'member_id=?', $this->member->member_id ) )->first() ); if ( !$referrer->member_id or $referrer->inGroup( explode( ',', \IPS\Settings::i()->nexus_no_commission ) ) ) { throw new \UnderflowException; } /* Get pertinent values */ $byNumber = \IPS\Db::i()->select( 'COUNT( i_id )', 'nexus_invoices', array( "i_member=? AND i_status=?", $referrer->member_id, static::STATUS_PAID ) )->first(); $byAmounts = iterator_to_array( \IPS\Db::i()->select( 'i_currency, SUM( i_total ) as value', 'nexus_invoices', array( "i_member=? AND i_status=?", $referrer->member_id, static::STATUS_PAID ), NULL, NULL, 'i_currency' )->setKeyField( 'i_currency' )->setValueField( 'value' ) ); $forNumber = \IPS\Db::i()->select( 'COUNT( i_id )', 'nexus_invoices', array( "i_member=? AND i_status=?", $this->member->member_id, static::STATUS_PAID ) )->first(); $forAmounts = iterator_to_array( \IPS\Db::i()->select( 'i_currency, SUM( i_total ) as value', 'nexus_invoices', array( "i_member=? AND i_status=?", $this->member->member_id, static::STATUS_PAID ), NULL, NULL, 'i_currency' )->setKeyField( 'i_currency' )->setValueField( 'value' ) ); /* Loop the rules to find out the best one */ $maxAmount = new \IPS\Math\Number('0'); $actingRule = NULL; foreach ( new \IPS\Patterns\ActiveRecordIterator( \IPS\Db::i()->select( '*', 'nexus_referral_rules', array( array( "rrule_by_group='*' OR " . \IPS\Db::i()->findInSet( 'rrule_by_group', $referrer->groups ) ), array( "rrule_for_group='*' OR " . \IPS\Db::i()->findInSet( 'rrule_for_group', $this->member->groups ) ) ) ), 'IPS\nexus\CommissionRule' ) as $commissionRule ) { /* Before we go nuts, don't bother if it's not going to give us more commission */ if ( $amountReceived->percentage( $commissionRule->commission )->compare( $maxAmount ) === -1 ) { continue; } if ( $commissionRule->commission_limit ) { $commisionLimit = json_decode( $commissionRule->commission_limit, TRUE ); if ( isset( $commisionLimit[ $this->currency ] ) and ( new \IPS\Math\Number( number_format( $commisionLimit[ $this->currency ]['amount'], \IPS\nexus\Money::numberOfDecimalsForCurrency( $this->currency ), '.', '' ) ) )->compare( $maxAmount ) === -1 ) { continue; } } /* Is the invoice amount right? */ if ( $commissionRule->purchase_amount_op ) { $amount = json_decode( $commissionRule->purchase_amount_unit, TRUE ); if ( !$this->_checkCondition( $this->total->amount, $commissionRule->purchase_amount_op, $amount[ $this->currency ] ) ) { continue; } } /* Does the referring user have the right amount of purchases? */ if ( $commissionRule->by_purchases_op ) { $val = $commissionRule->by_purchases_unit; if ( $commissionRule->by_purchases_type == 'v' ) { $val = json_decode( $val, TRUE ); $val = $val[ $this->currency ]; } if ( !$this->_checkCondition( ( $commissionRule->by_purchases_type == 'n' ? $byNumber : $byAmounts[ $this->currency ] ), $commissionRule->by_purchases_op, $val ) ) { continue; } } /* Does the purchasing user have the right amount of purchases? */ if ( $commissionRule->for_purchases_op ) { $val = $commissionRule->for_purchases_unit; if ( $commissionRule->for_purchases_type == 'v' ) { $val = json_decode( $val, TRUE ); $val = $val[ $this->currency ]; } if ( !$this->_checkCondition( ( $commissionRule->for_purchases_type == 'n' ? $forNumber : $forAmounts[ $this->currency ] ), $commissionRule->for_purchases_op, $val ) ) { continue; } } /* Work out what packages apply */ $amountToApplyCommissionOn = $amountReceived; if ( $commissionRule->purchase_packages ) { if ( $commissionRule->purchase_package_limit ) { $amountToApplyCommissionOn = new \IPS\Math\Number('0'); } $itemsToCount = array(); foreach ( $this->items as $k => $item ) { /* What's the package ID? */ if ( $item instanceof \IPS\nexus\extensions\nexus\Item\Package ) { $packageId = $item->id; } elseif ( $commissionRule->purchase_renewal and $item instanceof \IPS\nexus\Invoice\Item\Renewal and $item::$application == 'nexus' and $item::$type == 'package' ) { try { $packageId = \IPS\nexus\Purchase::load( $item->id )->item_id; } catch ( \OutOfRangeException $e ) { } } /* Is it in our list? */ if ( !$packageId or !in_array( $packageId, explode( ',', $commissionRule->purchase_packages ) ) ) { continue; } /* Add it to the list */ if ( $packageId ) { $itemsToCount[ $k ] = $packageId; } if ( $commissionRule->purchase_package_limit ) { $amountToApplyCommissionOn = $amountToApplyCommissionOn->add( $item->price->amount->multiply( new \IPS\Math\Number( "{$item->quantity}" ) ) ); } } /* If we need ALL of the packages, do we have them all? */ if ( $commissionRule->purchase_any ) { if ( empty( $itemsToCount ) ) { continue; } } else { foreach ( explode( ',', $commissionRule->purchase_packages ) as $pid ) { if ( !in_array( $pid, $itemsToCount ) ) { continue 2; } } } } /* If we're still here, we're applying commission */ $amountToGive = $amountToApplyCommissionOn->percentage( $commissionRule->commission ); if ( $commissionRule->commission_limit and $commissionRule->commission_limit != '*' ) { $limit = json_decode( $commissionRule->commission_limit, TRUE ); if ( isset( $limit[ $this->currency ] ) and $amountToGive->compare( new \IPS\Math\Number( number_format( $limit[ $this->currency ]['amount'], \IPS\nexus\Money::numberOfDecimalsForCurrency( $this->currency ), '.', '' ) ) ) === 1 ) { $amountToGive = new \IPS\Math\Number( number_format( $limit[ $this->currency ]['amount'], \IPS\nexus\Money::numberOfDecimalsForCurrency( $this->currency ), '.', '' ) ); } } /* Is this more than what we have? */ if ( ( $amountToGive->compare( $maxAmount ) === 1 ) or ( $amountToGive->isGreaterThanZero() and !$maxAmount->isGreaterThanZero() ) ) { $maxAmount = $amountToGive; $actingRule = $commissionRule; } } /* Return */ $extra = $this->status_extra; $extra['commissionrule'] = $actingRule ? $actingRule->id : 0; $extra['commissionref'] = $referrer->member_id; $extra['commissionamount'] = $maxAmount; $this->status_extra = $extra; } catch ( \UnderflowException $e ) { $extra = $this->status_extra; $extra['commissionrule'] = 0; $extra['commissionref'] = 0; $extra['commissionamount'] = 0; $this->status_extra = $extra; } $this->save(); } try { return array( 'rule' => $this->status_extra['commissionrule'] ? \IPS\nexus\CommissionRule::load( $this->status_extra['commissionrule'] ) : NULL, 'referrer' => $this->status_extra['commissionref'] ? \IPS\nexus\Customer::load( $this->status_extra['commissionref'] ) : NULL, 'amount' => $this->status_extra['commissionamount'] ? new \IPS\nexus\Money( $this->status_extra['commissionamount'], $this->currency ) : NULL ); } catch( \OutOfRangeException $e ) { /* If either the commission rule, or the commission ref, are no longer valid, then let's try again and see if it matches up with a different one. */ $extra = $this->status_extra; unset( $extra['commissionrule'], $extra['commissionref'], $extra['commissionamount'] ); $this->status_extra = $extra; $this->save(); return $this->commission( $amountReceived ); } } /** * Check condition * * @param float $a First parameter * @param string $operator Operator (g = greater than, e = equal to, l = less than) * @param float $b Second parameter * @return bool */ protected function _checkCondition( $a, $operator, $b ) { if ( $a instanceof \IPS\Math\Number and !( $b instanceof \IPS\Math\Number ) ) { $b = new \IPS\Math\Number( "{$b}" ); } if ( !( $a instanceof \IPS\Math\Number ) and $b instanceof \IPS\Math\Number ) { $a = new \IPS\Math\Number( "{$a}" ); } switch ( $operator ) { case 'g': if ( $a instanceof \IPS\Math\Number ) { return $a->compare( $b ) === 1; } else { return $a > $b; } case 'e': if ( $a instanceof \IPS\Math\Number ) { return $a->compare( $b ) === 0; } else { return $a == $b; } case 'l': if ( $a instanceof \IPS\Math\Number ) { return $a->compare( $b ) === 11; } else { return $a < $b; } } return FALSE; } /** * Get amount left to pay * * @return \IPS\nexus\Money */ public function amountToPay( $deductHeldTransactions=FALSE ) { $amountToPay = $this->total; $statuses = array( \IPS\nexus\Transaction::STATUS_PAID, \IPS\nexus\Transaction::STATUS_PART_REFUNDED ); if ( $deductHeldTransactions ) { $statuses[] = \IPS\nexus\Transaction::STATUS_HELD; $statuses[] = \IPS\nexus\Transaction::STATUS_REVIEW; $statuses[] = \IPS\nexus\Transaction::STATUS_GATEWAY_PENDING; } foreach ( $this->transactions( $statuses ) as $transaction ) { if ( $transaction->currency === $this->currency ) { $amountToSubtract = $transaction->amount->amount; if ( $transaction->status === \IPS\nexus\Transaction::STATUS_PART_REFUNDED ) { $amountToSubtract = $amountToSubtract->subtract( $transaction->partial_refund->amount ); } $amountToPay->amount = $amountToPay->amount->subtract( $amountToSubtract ); } } return $amountToPay; } /** * ACP Buttons * * @param string $ref Referer * @return array */ public function buttons( $ref='v' ) { $url = $this->acpUrl()->setQueryString( 'r', $ref ); $return = array(); if ( $this->status !== \IPS\nexus\Invoice::STATUS_PAID and \IPS\Member::loggedIn()->hasAcpRestriction( 'nexus', 'payments', 'invoices_add' ) ) { $return['edit'] = array( 'title' => 'edit', 'icon' => 'pencil', 'link' => $url->setQueryString( array( 'do' => 'generate', '_new' => 1 ) ) ); } if ( $this->status === \IPS\nexus\Invoice::STATUS_PENDING ) { if ( \IPS\Member::loggedIn()->hasAcpRestriction( 'nexus', 'payments', 'invoices_edit' ) ) { $return['paid'] = array( 'title' => 'invoice_mark_paid', 'icon' => 'check', 'link' => $url->setQueryString( 'do', 'paid' ), 'data' => count( $this->transactions( array( \IPS\nexus\Transaction::STATUS_PENDING, \IPS\nexus\Transaction::STATUS_WAITING, \IPS\nexus\Transaction::STATUS_HELD, \IPS\nexus\Transaction::STATUS_REVIEW, \IPS\nexus\Transaction::STATUS_GATEWAY_PENDING ) ) ) ? array( 'ipsDialog' => '', 'ipsDialog-title' => \IPS\Member::loggedIn()->language()->addToStack('invoice_mark_paid') ) : array( 'confirm' => '' ) ); } if ( count( \IPS\nexus\Gateway::manualChargeGateways() ) and \IPS\Member::loggedIn()->hasAcpRestriction( 'nexus', 'payments', 'chargetocard' ) ) { $return['card'] = array( 'title' => 'invoice_charge_to_card', 'icon' => 'credit-card', 'link' => $url->setQueryString( array( 'do' => 'card', '_new' => 1 ) ), ); } if ( isset( $this->member->cm_credits[ $this->currency ] ) and $this->member->cm_credits[ $this->currency ]->amount->isGreaterThanZero() and \IPS\Member::loggedIn()->hasAcpRestriction( 'nexus', 'payments', 'invoices_edit' ) ) { $return['credit'] = array( 'title' => 'invoice_charge_to_credit', 'icon' => 'money', 'link' => $url->setQueryString( 'do', 'credit' ), 'data' => array( 'ipsDialog' => '', 'ipsDialog-title' => \IPS\Member::loggedIn()->language()->addToStack('invoice_charge_to_credit') ) ); } } if ( $this->status !== \IPS\nexus\Invoice::STATUS_PAID and \IPS\Member::loggedIn()->hasAcpRestriction( 'nexus', 'payments', 'invoices_resend' ) ) { $return['resend'] = array( 'title' => 'invoice_reissue', 'icon' => 'refresh', 'link' => $url->setQueryString( 'do', 'resend' ), 'data' => array( 'confirm' => '', 'confirmSubMessage' => \IPS\Member::loggedIn()->language()->addToStack('invoice_reissue_confirm'), 'confirmType' => 'verify', 'confirmIcon' => 'question', 'confirmButtons' => json_encode( array( 'yes' => \IPS\Member::loggedIn()->language()->addToStack('invoice_reissue_yes'), 'no' => \IPS\Member::loggedIn()->language()->addToStack('invoice_reissue_no'), 'cancel' => \IPS\Member::loggedIn()->language()->addToStack('cancel'), ) ) ) ); } $return['print'] = array( 'title' => 'print', 'icon' => 'print', 'target' => '_blank', 'link' => $url->setQueryString( 'do', 'printout' ), ); if ( $this->status !== \IPS\nexus\Invoice::STATUS_CANCELED and \IPS\Member::loggedIn()->hasAcpRestriction( 'nexus', 'payments', 'invoices_edit' ) ) { $return['unpaid'] = array( 'title' => $this->status === \IPS\nexus\Invoice::STATUS_PAID ? 'invoice_mark_unpaid' : 'cancel', 'icon' => 'times', 'link' => $url->setQueryString( 'do', 'unpaid' ), 'data' => array( 'ipsDialog' => '', 'ipsDialog-title' => \IPS\Member::loggedIn()->language()->addToStack('cancel') ) ); } if ( \IPS\Member::loggedIn()->hasAcpRestriction( 'nexus', 'payments', 'invoices_delete' ) ) { $return['delete'] = array( 'title' => 'delete', 'icon' => 'times-circle', 'link' => $url->setQueryString( 'do', 'delete' ), 'data' => array( 'confirm' => '', 'confirmSubMessage' => \IPS\Member::loggedIn()->language()->addToStack('invoice_delete_warning') ) ); } return $return; } /** * Can payment be split? Returns the minnimum split amount * * @return FALSE|float */ public function canSplitPayment() { if ( \IPS\Settings::i()->nexus_split_payments == -1 ) { return FALSE; } if ( \IPS\Settings::i()->nexus_split_payments ) { $decoded = json_decode( \IPS\Settings::i()->nexus_split_payments, TRUE ); if ( isset( $decoded[ $this->currency ] ) ) { if ( $decoded[ $this->currency ]['amount'] <= $this->amountToPay()->amount ) { return $decoded[ $this->currency ]['amount']; } } return FALSE; } else { return 0.01; } } /** * Get output for API * * @return array * @apiresponse int id ID number * @apiresponse string title Title * @apiresponse string status 'paid' = Paid; 'pend' = Pending; 'expd' = Expired; 'canc' = Cancelled * @apiresponse datetime issueDate The date the invoice was issued * @apiresponse datetime paidDate The date the invoice was paid * @apiresponse \IPS\nexus\Customer customer Customer * @apiresponse [\IPS\nexus\Invoice\Item] items Items * @apiresponse \IPS\nexus\Money subTotal Subtotal of item costs * @apiresponse [\IPS\nexus\Invoice\Item] shippingCharges Shipping charges * @apiresponse \IPS\nexus\Money shippingTotal Total cost of shipping charges * @apiresponse [\IPS\nexus\Invoice\Item\TaxItem] tax Tax charges * @apiresponse \IPS\nexus\Money taxTotal Total cost of tax charges * @apiresponse \IPS\nexus\Money total Total * @apiresponse \IPS\nexus\Money outstanding The outstanding amount to be paid * @apiresponse \IPS\GeoLocation billingAddress Billing address * @apiresponse \IPS\GeoLocation shippingAddress Shipping Address * @apiresponse string poNumber PO Number (set by customer) * @apiresponse string notes Notes (set by customer) * @apiresponse [\IPS\nexus\Transaction] transactions Transactions against this invoice * @apiresponse [\IPS\nexus\Shipping\Order] shipments Shipments generated from this invoice (if paid) * @apiresponse [\IPS\nexus\Purchase] purchases Purchases generated from this invoice (if paid) * @apiresponse string viewUrl The URL to where the customer can view this invoice * @apiresponse string checkoutUrl The URL to where the customer can pay this invoice */ public function apiOutput() { $summary = $this->summary(); $tax = array(); foreach ( $summary['tax'] as $classId => $data ) { $tax[] = ( new \IPS\nexus\Invoice\Item\TaxItem( $classId, $data ) )->apiOutput(); } return array( 'id' => $this->id, 'title' => $this->title, 'status' => $this->status, 'issueDate' => $this->date->rfc3339(), 'paidDate' => $this->paid ? $this->paid->rfc3339() : null, 'customer' => $this->member->apiOutput(), 'items' => array_map( function( $item ){ return $item->apiOutput(); }, $summary['items'] ), 'subTotal' => $summary['subtotal']->apiOutput(), 'shippingCharges' => array_map( function( $item ){ return $item->apiOutput(); }, $summary['shipping'] ), 'shippingTotal' => $summary['shippingTotal']->apiOutput(), 'tax' => $tax, 'taxTotal' => $summary['taxTotal']->apiOutput(), 'total' => $summary['total']->apiOutput(), 'outstanding' => $this->amountToPay()->apiOutput(), 'billingAddress' => $this->billaddress ? $this->billaddress->apiOutput() : null, 'shippingAddress' => $this->shipaddress ? $this->shipaddress->apiOutput() : null, 'poNumber' => $this->po, 'notes' => $this->notes, 'transactions' => array_map( function( $transaction ){ return $transaction->apiOutput(); }, iterator_to_array( $this->transactions() ) ), 'shipments' => array_map( function( $shipment ){ return $shipment->apiOutput(); }, iterator_to_array( $this->shipments() ) ), 'purchases' => array_map( function( $purchase ){ return $purchase->apiOutput(); }, iterator_to_array( $this->purchasesCreated() ) ), 'viewUrl' => (string) $this->url(), 'checkoutUrl' => (string) $this->checkoutUrl(), ); } /* !Related Resources */ /** * Get transactions * * @param array|null $statuses Statuses to get, or NULL for all * @param array $where Initial where clause * @return \IPS\Patterns\ActiveRecordIteator */ public function transactions( $statuses=NULL, $where = array() ) { $where[] = array( 't_invoice=?', $this->id ); if ( $statuses ) { $where[] = \IPS\Db::i()->in( 't_status', $statuses ); } return new \IPS\Patterns\ActiveRecordIterator( \IPS\Db::i()->select( '*', 'nexus_transactions', $where ), 'IPS\nexus\Transaction' ); } /** * Get shipments * * @param array|null $statuses Statuses to get, or NULL for all * @return \IPS\Patterns\ActiveRecordIteator */ public function shipments( $statuses=NULL ) { $where = array(); $where[] = array( 'o_invoice=?', $this->id ); if ( $statuses ) { $where[] = \IPS\Db::i()->in( 'o_status', $statuses ); } return new \IPS\Patterns\ActiveRecordIterator( \IPS\Db::i()->select( '*', 'nexus_ship_orders', $where ), 'IPS\nexus\Shipping\Order' ); } /** * Get purchases created by this invoice * * @return \IPS\Patterns\ActiveRecordIteator */ public function purchasesCreated() { return new \IPS\Patterns\ActiveRecordIterator( \IPS\Db::i()->select( '*', 'nexus_purchases', array( 'ps_original_invoice=? AND ps_show=1', $this->id ) ), 'IPS\nexus\Purchase' ); } /* !Actions */ /** * Create account from guest checkout * * @return \IPS\nexus\Customer */ public function createAccountForGuest() { $profileFields = $this->guest_data['profileFields']; $securityAnswers = isset( $this->guest_data['securityAnswers'] ) ? $this->guest_data['securityAnswers'] : array(); /* If the member has already been created (the user made two separate purchases with the same data), use that, otherwise create one */ try { $memberToSave = \IPS\nexus\Customer::load( $this->guest_data['member']['email'], 'email' ); } catch ( \OutOfRangeException $e ) { $memberToSave = new \IPS\nexus\Customer; foreach ( $this->guest_data['member'] as $k => $v ) { $memberToSave->_data[ $k ] = $v; $memberToSave->changed[ $k ] = $v; } $memberToSave->save(); } $this->member = $memberToSave; $this->guest_data = NULL; $this->save(); /* If we provided answers to security questions in check out, save those */ if ( \IPS\Settings::i()->security_questions_enabled and in_array( \IPS\Settings::i()->security_questions_prompt, array( 'register', 'optional' ) ) ) { if ( $securityAnswers ) { foreach ( $securityAnswers as $k => $v ) { \IPS\Db::i()->insert( 'core_security_answers', array( 'answer_question_id' => $k, 'answer_member_id' => $this->member->member_id, 'answer_answer' => $v ) ); } $this->member->members_bitoptions['has_security_answers'] = TRUE; /* Log MFA Enable */ $this->member->logHistory( 'core', 'mfa', array( 'handler' => 'questions', 'enable' => TRUE ) ); } else { $this->member->members_bitoptions['security_questions_opt_out'] = TRUE; /* Log MFA Optout */ $this->member->logHistory( 'core', 'mfa', array( 'handler' => 'questions', 'enable' => FALSE, 'optout' => TRUE ) ); } $this->member->save(); } /* If we've entered an address during checkout, save it */ if ( $this->billaddress !== NULL ) { $billing = new \IPS\nexus\Customer\Address; $billing->member = $this->member; $billing->address = $this->billaddress; $billing->primary_billing = 1; $billing->save(); } if ( $this->shipaddress !== NULL ) { $shipping = new \IPS\nexus\Customer\Address; $shipping->member = $this->member; $shipping->address = $this->shipaddress; $shipping->primary_shipping = 1; $shipping->save(); } $profileFields['member_id'] = $memberToSave->member_id; \IPS\Db::i()->replace( 'core_pfields_content', $profileFields ); /* Validation */ if ( \IPS\Settings::i()->nexus_checkreg_validate or !$this->total->amount->isGreaterThanZero() ) { $this->member->postRegistration( FALSE, TRUE ); } else if( \IPS\Settings::i()->new_reg_notify ) { \IPS\Email::buildFromTemplate( 'core', 'registration_notify', array( $memberToSave, $profileFields ), \IPS\Email::TYPE_LIST )->send( \IPS\Settings::i()->email_in ); } /* Update associated transactions */ \IPS\Db::i()->update( 'nexus_billing_agreements', array( 'ba_member' => $this->member->member_id ), array( 'ba_id IN(?)', \IPS\Db::i()->select( 't_billing_agreement', 'nexus_transactions', array( 't_billing_agreement>0 AND t_invoice=? AND t_member=0', $this->id ) ) ) ); \IPS\Db::i()->update( 'nexus_transactions', array( 't_member' => $this->member->member_id ), array( 't_invoice=? AND t_member=0', $this->id ) ); /* Did we use any coupons? */ foreach ( $this->items as $item ) { if ( $item instanceof \IPS\nexus\extensions\nexus\Item\CouponDiscount ) { $coupon = \IPS\nexus\Coupon::load( $item->id ); $uses = $coupon->used_by ? json_decode( $coupon->used_by, TRUE ) : array(); if ( isset( $uses[ $this->member->email ] ) ) { $uses[ $this->member->member_id ] = $uses[ $this->member->email ]; unset( $uses[ $this->member->email ] ); } else { $uses[ $this->member->member_id ] = 1; } $coupon->used_by = json_encode( $uses ); $coupon->save(); } } return $memberToSave; } /** * Mark as paid * * @param \IPS\Member|NULL $member The member changing the status. Only set if the status is being manually changed by an admin. * @return \IPS\Member|NULL If the invoice belonged to a guest, a member will be created by approving and returned here * @note It is the responsibility of the code calling this to log the action on the customer's account *before* calling this method */ public function markPaid( \IPS\Member $member = NULL ) { $return = NULL; if ( $this->status !== static::STATUS_PAID ) { /* Create the member account if this was a guest */ if ( !$this->member->member_id and $this->guest_data and $this->requiresLogin() ) { $return = $this->createAccountForGuest(); } /* Init */ $extra = $this->status_extra; $shipOrders = array(); /* Find any billing agreements */ $billingAgreement = NULL; foreach ( $this->transactions( array( \IPS\nexus\Transaction::STATUS_PAID, \IPS\nexus\Transaction::STATUS_PART_REFUNDED ), array( array( 't_billing_agreement IS NOT NULL' ) ) ) as $transaction ) { $billingAgreement = $transaction->billing_agreement; } /* Loop items */ $purchases = array(); $setParentIds = array(); $groupParentIds = array(); $amountReceived = $this->total->amount; foreach ( $this->items as $k => $item ) { /* onPaid */ $item->onPaid( $this ); /* Create a purchase record */ $purchase = NULL; if( $item instanceof \IPS\nexus\Invoice\Item\Purchase ) { $showPurchaseRecord = $item->showPurchaseRecord(); for ( $i = 0; $i < $item->quantity; $i++ ) { /* Create a purchase record */ $purchase = new \IPS\nexus\Purchase; $purchase->member = $this->member; $purchase->name = $item->name; if ( $item->renewalTerm ) { $purchase->renewals = $item->renewalTerm; $purchase->expire = \IPS\DateTime::create()->add( $item->renewalTerm->interval ); if ( $item->renewalTerm->gracePeriod ) { $purchase->grace_period = \IPS\DateTime::create()->add( $item->renewalTerm->gracePeriod )->getTimestamp() - time(); } if ( $billingAgreement ) { $purchase->billing_agreement = $billingAgreement; } } if ( $item->expireDate ) { $purchase->expire = $item->expireDate; } $purchase->app = $item::$application; $purchase->type = $item::$type; $purchase->item_id = intval( $item->id ); $purchase->custom_fields = $item->details; $purchase->original_invoice = $this; if ( $item->payTo ) { $purchase->pay_to = $item->payTo; $purchase->commission = $item->commission; } if ( $item->parent !== NULL ) { if ( $item->parent instanceof \IPS\nexus\Purchase ) { $purchase->parent = $item->parent; } else { $setParentIds[ $k ] = $item->parent; if ( $item->groupWithParent ) { $groupParentIds[ $k ] = TRUE; } } } $purchase->extra = $item->extra; $purchase->show = (int) $showPurchaseRecord; $purchase->save(); if( $purchase->parent and $item->groupWithParent ) { $purchase->groupWithParent(); } /* Make a note of the ID */ if ( !isset( $purchases[ $k ] ) ) { $purchases[ $k ] = array(); } $purchases[ $k ][] = $purchase; /* Claim attachments */ foreach ( \IPS\Db::i()->select( '*', 'core_attachments_map', array( 'location_key=? AND id1=? AND id3=?', 'nexus_Purchases', $k, "invoice-{$this->id}" ) ) as $attachMap ) { $attachMap['id1'] = $purchase->id; $attachMap['id3'] = 'purchase'; \IPS\Db::i()->insert( 'core_attachments_map', $attachMap ); } /* Log */ if ( $showPurchaseRecord ) { $this->member->log( 'purchase', array( 'type' => 'new', 'id' => $purchase->id, 'name' => $purchase->name, 'invoice_id' => $this->id, 'invoice_title' => $this->title ), FALSE ); } } } /* Does it need shipping? */ if ( $item->physical ) { if ( !isset( $shipOrders[ $item->chosenShippingMethodId ] ) ) { $shipOrders[ $item->chosenShippingMethodId ] = array(); } $shipOrders[ $item->chosenShippingMethodId ][ $k ] = $item; } /* Is the money going to another member? */ if ( $item->payTo and !$item->payTo->inGroup( explode( ',', \IPS\Settings::i()->nexus_no_commission ) ) ) { $recipientAmounts = $item->recipientAmounts(); $credits = $item->payTo->cm_credits; $credits[ $this->currency ]->amount = $credits[ $this->currency ]->amount->add( $recipientAmounts['recipient_final']->amount ); $item->payTo->cm_credits = $credits; $item->payTo->save(); $amountReceived = $amountReceived->subtract( $recipientAmounts['recipient_final']->amount ); if ( !isset( $extra['commission'][ $item->payTo->member_id ] ) ) { $extra['commission'][ $item->payTo->member_id ] = array( 'amount' => new \IPS\Math\Number('0') ); } $extra['commission'][ $item->payTo->member_id ]['amount'] = $extra['commission'][ $item->payTo->member_id ]['amount']->add( $recipientAmounts['recipient_final']->amount ); $item->payTo->log( 'comission', array( 'type' => 'purchase', 'currency' => $this->currency, 'amount' => $recipientAmounts['recipient_final']->amount, 'new_amount' => $credits[ $this->currency ]->amount, 'member' => $this->member->member_id, 'id' => $purchase ? $purchase->id : 0, 'name' => $item->name ), FALSE ); \IPS\Email::buildFromTemplate( 'nexus', 'commissionEarned', array( $this, $item, $recipientAmounts['recipient_final'] ), \IPS\Email::TYPE_TRANSACTIONAL ) ->send( $item->payTo, array_map( function( $contact ) { return $contact->alt_id->email; }, iterator_to_array( $item->payTo->alternativeContacts( array( 'billing=1' ) ) ) ), ( ( in_array( 'commission_earned', explode( ',', \IPS\Settings::i()->nexus_notify_copy_types ) ) AND \IPS\Settings::i()->nexus_notify_copy_email ) ? explode( ',', \IPS\Settings::i()->nexus_notify_copy_email ) : array() ) ); } } /* Referral Commission */ if ( \IPS\Settings::i()->cm_ref_on and $commission = $this->commission( $amountReceived ) and $commission['amount'] ) { $credits = $commission['referrer']->cm_credits; $credits[ $this->currency ]->amount = $credits[ $this->currency ]->amount->add( $commission['amount']->amount ); $commission['referrer']->cm_credits = $credits; $commission['referrer']->save(); if ( !isset( $extra['commission'][ $commission['referrer']->member_id ] ) ) { $extra['commission'][ $commission['referrer']->member_id ] = array( 'amount' => new \IPS\Math\Number('0') ); } $extra['commission'][ $commission['referrer']->member_id ]['amount'] = $extra['commission'][ $commission['referrer']->member_id ]['amount']->add( $commission['amount']->amount ); $commission['referrer']->log( 'comission', array( 'type' => 'invoice', 'currency' => $this->currency, 'amount' => $commission['amount']->amount, 'new_amount' => $credits[ $this->currency ]->amount, 'member' => $this->member->member_id, 'invoice_id' => $this->id, 'invoice_title' => $this->title ), FALSE ); $record = \IPS\Db::i()->select( '*', 'nexus_referrals', array( 'member_id=? AND referred_by=?', $this->member->member_id, $commission['referrer']->member_id ) )->first(); $amounts = $record['amount'] ? json_decode( $record['amount'], TRUE ) : array(); if ( !isset( $amounts[ $this->currency ] ) ) { $amounts[ $this->currency ] = new \IPS\Math\Number('0'); } elseif ( !( $amounts[ $this->currency ] instanceof \IPS\Math\Number ) ) { $amounts[ $this->currency ] = new \IPS\Math\Number( number_format( $amounts[ $this->currency ], \IPS\nexus\Money::numberOfDecimalsForCurrency( $this->currency ), '.', '' ) ); } $amounts[ $this->currency ] = $amounts[ $this->currency ]->add( $commission['amount']->amount ); \IPS\Db::i()->update( 'nexus_referrals', array( 'amount' => json_encode( $amounts ) ), array( 'member_id=? AND referred_by=?', $this->member->member_id, $commission['referrer']->member_id ) ); \IPS\Email::buildFromTemplate( 'nexus', 'commissionEarned', array( $this, NULL, $commission['amount'] ), \IPS\Email::TYPE_TRANSACTIONAL ) ->send( $commission['referrer'], array_map( function( $contact ) { return $contact->alt_id->email; }, iterator_to_array( $commission['referrer']->alternativeContacts( array( 'billing=1' ) ) ) ), ( ( in_array( 'commission_earned', explode( ',', \IPS\Settings::i()->nexus_notify_copy_types ) ) AND \IPS\Settings::i()->nexus_notify_copy_email ) ? explode( ',', \IPS\Settings::i()->nexus_notify_copy_email ) : array() ) ); } /* Change commission values back to numbers */ if ( isset( $extra['commission'] ) ) { foreach ( $extra['commission'] as $memberId => $data ) { if ( $data['amount'] and $data['amount'] instanceof \IPS\Math\Number ) { $extra['commission'][ $memberId ]['amount'] = (string) $data['amount']; } } } /* Set parent IDs */ foreach ( $setParentIds as $k => $v ) { foreach ( $purchases[ $k ] as $k2 => $_purchase ) { $_purchase->parent = isset( $purchases[ $v ][ $k2 ] ) ? $purchases[ $v ][ $k2 ] : $purchases[ $v ][0]; $_purchase->save(); if( isset( $groupParentIds[ $k ] ) ) { $_purchase->groupWithParent(); } } } /* Run callbacks */ foreach ( $purchases as $_purchases ) { foreach ( $_purchases as $purchase ) { $purchase->onPurchaseGenerated( $this ); } } /* Create shipping orders */ foreach ( $shipOrders as $method => $items ) { $shipOrder = new \IPS\nexus\Shipping\Order; $shipOrder->invoice = $this; $shipOrder->data = array( 'cm_first_name' => $this->member->cm_first_name, 'cm_last_name' => $this->member->cm_last_name, 'address' => $this->shipaddress, 'cm_phone' => $this->member->cm_phone ); if ( $method ) { try { $shipOrder->method = \IPS\nexus\Shipping\FlatRate::load( $method ); } catch ( \OutOfRangeException $e ) { $shipOrder->api_service = $method; } } $shipOrder->items = $items; $shipOrder->save(); $this->member->log( 'shipping', array( 'type' => 'new', 'id' => $shipOrder->id, 'invoice_id' => $this->id, 'invoice_title' => $this->title ), FALSE ); } /* Refuse any pending transactions */ foreach ( $this->transactions( array( \IPS\nexus\Transaction::STATUS_PENDING, \IPS\nexus\Transaction::STATUS_WAITING, \IPS\nexus\Transaction::STATUS_HELD, \IPS\nexus\Transaction::STATUS_REVIEW, \IPS\nexus\Transaction::STATUS_GATEWAY_PENDING ) ) as $transaction ) { if ( $transaction->status === \IPS\nexus\Transaction::STATUS_PENDING ) { $transaction->delete(); } else { try { $transaction->method->void( $transaction ); } catch ( \Exception $e ){} $textra = $transaction->extra; $textra['history'][] = array( 's' => \IPS\nexus\Transaction::STATUS_REFUSED, 'on' => time(), 'note' => 'invoicePaid' ); $transaction->extra = $textra; $transaction->status = \IPS\nexus\Transaction::STATUS_REFUSED; $transaction->save(); } } /* Update invoice */ $this->status = static::STATUS_PAID; $this->paid = new \IPS\DateTime; /* Log */ if ( $member ) { $extra['type'] = 'manual'; $extra['setTo'] = static::STATUS_PAID; $extra['setByID'] = $member->member_id; $extra['date'] = time(); } /* Save */ $this->status_extra = $extra; $this->save(); } return $return; } /** * Mark as unpaid * * @param string|NULL $status The status to set to * @param \IPS\Member|NULL $member The member changing the status. Only set if the status is being manually changed by an admin. * @return void */ public function markUnpaid( $status, \IPS\Member $member = NULL ) { /* Do stuff */ if ( $this->status === static::STATUS_PAID ) { /* Loop items */ foreach ( $this->items as $k => $item ) { $item->onUnpaid( $this, $this->status ); /* Is the money going to another member? */ if ( $item->payTo and !$item->payTo->inGroup( explode( ',', \IPS\Settings::i()->nexus_no_commission ) ) ) { $recipientAmounts = $item->recipientAmounts(); $credits = $item->payTo->cm_credits; $credits[ $this->currency ]->amount = $credits[ $this->currency ]->amount->subtract( $recipientAmounts['recipient_final']->amount ); $item->payTo->cm_credits = $credits; $item->payTo->save(); $item->payTo->log( 'comission', array( 'type' => 'invoice_refund', 'currency' => $this->currency, 'amount' => $recipientAmounts['recipient_final']->amount, 'new_amount' => $credits[ $this->currency ]->amount, 'member' => $this->member->member_id, 'invoice_id' => $this->id, 'invoice_title' => $this->title ), FALSE ); \IPS\Email::buildFromTemplate( 'nexus', 'commissionRevoked', array( $this, $item, $recipientAmounts['recipient_final'] ), \IPS\Email::TYPE_TRANSACTIONAL ) ->send( $item->payTo, array_map( function( $contact ) { return $contact->alt_id->email; }, iterator_to_array( $item->payTo->alternativeContacts( array( 'billing=1' ) ) ) ), ( ( in_array( 'commission_earned', explode( ',', \IPS\Settings::i()->nexus_notify_copy_types ) ) AND \IPS\Settings::i()->nexus_notify_copy_email ) ? explode( ',', \IPS\Settings::i()->nexus_notify_copy_email ) : array() ) ); } } /* Delete purchases */ foreach ( $this->purchasesCreated() as $purchase ) { $purchase->delete(); } /* Delete shipping orders */ foreach ( $this->shipments() as $shipment ) { $shipment->delete(); } } else { foreach ( $this->items as $k => $item ) { $item->onInvoiceCancel( $this ); } } /* Set the status */ $this->status = $status; /* Log */ if ( $member ) { $extra = $this->status_extra; $extra['type'] = 'manual'; $extra['setTo'] = $status; $extra['setByID'] = $member->member_id; $extra['date'] = time(); $this->status_extra = $extra; } /* Save */ $this->save(); } /** * What will happen if we mark this unpaid? * * @return array */ public function unpaidConsequences() { $return = array(); $purchases = $this->purchasesCreated(); if ( count( $purchases ) ) { $return['purchases'] = array(); foreach ( $purchases as $purchase ) { $return['purchases'][] = \IPS\Theme::i()->getTemplate('purchases')->link( $purchase ); } } $shipments = $this->shipments(); if ( count( $shipments ) ) { $return['shipments'] = array(); foreach ( $shipments as $shipment ) { if ( $shipment->status === \IPS\nexus\Shipping\Order::STATUS_SHIPPED ) { $return['shipments'][] = \IPS\Theme::i()->getTemplate('shiporders')->link( $shipment ); } else { $return['shipments'][] = array( 'message' => \IPS\Theme::i()->getTemplate('shiporders')->link( $shipment ), 'warning' => \IPS\Member::loggedIn()->language()->addToStack('invoice_unpaid_shipments_shipped'), ); } } } foreach ( $this->items as $item ) { if ( $item->payTo ) { $amount = $item->amountForRecipient(); $message = \IPS\Member::loggedIn()->language()->addToStack('account_credit_remove', FALSE, array( 'sprintf' => array( $amount, $item->payTo->cm_name ) ) ); $credits = $item->payTo->cm_credits; if ( !$credits[ $amount->currency ]->amount->subtract( $amount->amount )->isPositive() ) { $return[] = array( 'message' => $message, 'warning' => \IPS\Member::loggedIn()->language()->addToStack('account_credit_remove_neg') ); } else { $return[] = $message; } } if( count( $item->onUnpaidDescription( $this ) ) ) { foreach( $item->onUnpaidDescription( $this ) as $desc ) { $return[] = $desc; } } } return $return; } /** * Send the notification to the member that this invoice has been generated * * @return void */ public function sendNotification() { \IPS\Email::buildFromTemplate( 'nexus', 'newInvoice', array( $this, $this->summary() ), \IPS\Email::TYPE_TRANSACTIONAL ) ->send( $this->member, array_map( function( $contact ) { return $contact->alt_id->email; }, iterator_to_array( $this->member->alternativeContacts( array( 'billing=1' ) ) ) ), ( ( in_array( 'new_invoice', explode( ',', \IPS\Settings::i()->nexus_notify_copy_types ) ) AND \IPS\Settings::i()->nexus_notify_copy_email ) ? explode( ',', \IPS\Settings::i()->nexus_notify_copy_email ) : array() ) ); } /* !URLS */ /** * Get checkout URL * * @return void */ public function checkoutUrl() { if ( !$this->id ) { $this->save(); } return \IPS\Http\Url::internal( 'app=nexus&module=checkout&controller=checkout&id=' . $this->id, 'front', 'nexus_checkout', array(), \IPS\Settings::i()->nexus_https ); } /** * @brief Cached URL */ protected $_url = NULL; /** * Get URL * * @return \IPS\Http\Url */ public function url() { if( $this->_url === NULL ) { $this->_url = \IPS\Http\Url::internal( "app=nexus&module=clients&controller=invoices&do=view&id={$this->id}", 'front', 'clientsinvoice' ); } return $this->_url; } /** * ACP "View Invoice" URL * * @return \IPS\Http\Url */ public function acpUrl() { return \IPS\Http\Url::internal( "app=nexus&module=payments&controller=invoices&do=view&id={$this->id}", 'admin' ); } }