<commit_before><?php

/**
 * @package         Billing
 * @copyright       Copyright (C) 2012-2016 BillRun Technologies Ltd. All rights reserved.
 * @license         GNU Affero General Public License Version 3; see LICENSE.txt
 */

/**
 * BillRun Bill class
 *
 * @package  Billrun
 * @since    5.0
 */
abstract class Billrun_Bill {

	/**
	 *
	 * @var string
	 */
	protected $type;

	/**
	 *
	 * @var Mongodloid_Entity
	 */
	protected $data;

	/**
	 *
	 * @var Mongodloid_Collection
	 */
	protected $billsColl;

	/**
	 * Optional fields to be saved to the payment. For some payment methods they are mandatory.
	 * @var array
	 */
	protected $optionalFields = array();

	const precision = 0.00001;
	
	/**
	 * 
	 * @param type $options
	 */
	public function __construct($options) {
		$this->updateLeft();
		$this->updateLeftToPay();
		$this->recalculatePaymentFields();
	}

	public function getRawData() {
		return $this->data->getRawData();
	}

	/**
	 * 
	 * @param array $rawData
	 */
	protected function setRawData($rawData) {
		$this->data = new Mongodloid_Entity($rawData, $this->billsColl);
	}

	public function getBIC() {
		if (isset($this->data['BIC'])) {
			return $this->data['BIC'];
		}
		return NULL;
	}

	public function getAmount() {
		if (isset($this->data['amount'])) {
			return $this->data['amount'];
		}
		return NULL;
	}

	public function getDue() {
		if (isset($this->data['due'])) {
			return $this->data['due'];
		}
		return 0;
	}

	public function getTime() {
		if (isset($this->data['urt'])) {
			return $this->data['urt'];
		}
		return NULL;
	}

	public function getAccountNo() {
		if (isset($this->data['aid'])) {
			return $this->data['aid'];
		}
		return NULL;
	}

	public function getIBAN() {
		if (isset($this->data['IBAN'])) {
			return $this->data['IBAN'];
		}
		return NULL;
	}

	public function getRUM() {
		if (isset($this->data['RUM'])) {
			return $this->data['RUM'];
		}
		return NULL;
	}

	public function getBankName() {
		if (isset($this->data['bank_name'])) {
			return $this->data['bank_name'];
		}
		return NULL;
	}

	public function getCustomerName() {
		if (isset($this->data['payer_name'])) {
			return $this->data['payer_name'];
		}
		return NULL;
	}

	public function getCustomerAddress() {
		if (isset($this->data['aaddress'])) {
			return $this->data['aaddress'];
		}
		return NULL;
	}

	public function getCustomerZipCode() {
		if (isset($this->data['azip'])) {
			return $this->data['azip'];
		}
		return NULL;
	}

	public function getCustomerCity() {
		if (isset($this->data['acity'])) {
			return $this->data['acity'];
		}
		return NULL;
	}

	public function getCustomerBankName() {
		if (isset($this->data['bank_name'])) {
			return $this->data['bank_name'];
		}
		return NULL;
	}

	public function save() {
		$this->data->save(NULL, 1);
		return true;
	}

	abstract public function getId();

	public static function getTotalDue($query = array(), $minBalance = false, $notFormatted = false) {
		$billsColl = Billrun_Factory::db()->billsCollection();
		$query = array(
			'$match' => $query,
		);
		$project1 = array(
			'$project' => array(
				'aid' => 1,
				'waiting_for_confirmation' => 1,
				'due' => 1,
				'waiting_for_confirmation_total' => array('$cond' => array('if' => array('$ne' => array('$waiting_for_confirmation', true)), 'then' => '$due' , 'else' => 0)),
				'pending_total' => array('$cond' => array('if' => array('$eq' => array('$pending', true)), 'then' => '$due' , 'else' => 0)),

			),
		);
		$group = array(
			'$group' => array(
				'_id' => '$aid',
				'total' => array(
					'$sum' => '$due',
				),
				'waiting_for_confirmation_total' => array(
					'$sum' => '$waiting_for_confirmation_total',
				),
				'pending_total' => array(
					'$sum' => '$pending_total',
				),
			),
		);
		
		$project2 = array(
			'$project' => array(
				'_id' => 0,
				'aid' => '$_id',
				'total' => 1,
				'waiting_for_confirmation_total' =>  1,
				'pending_total' => 1
			),
		);

		$having = array('$match' => array());
		if ($minBalance !== FALSE) {
			$having['$match']['total'] = array(
				'$gte' => $minBalance,
			);
		}
		if ($having['$match']) {
			$results = $billsColl->aggregate($query, $project1, $group, $project2, $having);
		} else {
			$results = $billsColl->aggregate($query,$project1, $group, $project2);
		}
		
		$results = iterator_to_array($results);
		if (!$notFormatted) {
			$results = array_map(function($ele) {
				$ele['total'] = Billrun_Util::getChargableAmount($ele['total']);
				$ele['waiting_for_confirmation_total'] = Billrun_Util::getChargableAmount($ele['waiting_for_confirmation_total']);
				$ele['pending_total'] = Billrun_Util::getChargableAmount($ele['pending_total']);
				return $ele;
			}, $results);
		}
		return array_combine(array_map(function($ele) {
				return $ele['aid'];
			}, $results), $results);
	}

	/**
	 * Return total / total waiting due for account
	 * @param int $aid
	 * @param boolean $notFormatted
	 * @return array
	 */
	public static function getTotalDueForAccount($aid, $date = null, $notFormatted = false) {
		$query = array('aid' => $aid);
		if (!empty($date)) {
			$query['$or'] = array(
				array('charge.not_before' => array('$lte' => new MongoDate(strtotime($date)))),
				array('charge.not_before' => array('$exists' => false)),
			);
		}
		$results = static::getTotalDue($query, $notFormatted);
		if (count($results)) {
			$total =  current($results)['total'];
			$totalWaiting = current($results)['waiting_for_confirmation_total'];
			$totalPending = abs(current($results)['pending_total']);
		} else if ($notFormatted) {
			$total = $totalWaiting = $totalPending = 0;
		} else {
			$total = $totalWaiting = $totalPending = Billrun_Util::getChargableAmount(0);
		}
		return array('total' => $total, 'without_waiting' => $totalWaiting, 'total_pending_amount' => $totalPending);
	}

	public static function payUnpaidBillsByOverPayingBills($aid, $sortByUrt = true) {
		$query = array(
			'aid' => $aid,
		);
		$sort = array(
			'urt' => 1,
		);
		$unpaidBills = Billrun_Bill::getUnpaidBills($query, $sort);
		$overPayingBills = Billrun_Bill::getOverPayingBills($query, $sort);
		foreach ($unpaidBills as $key1 => $unpaidBillRaw) {
			$unpaidBill = Billrun_Bill::getInstanceByData($unpaidBillRaw);
			$unpaidBillLeft = $unpaidBill->getLeftToPay();
			foreach ($overPayingBills as $key2 => $overPayingBill) {
				$payingBillAmountLeft = $overPayingBill->getLeft();
				if ($payingBillAmountLeft && (Billrun_Util::isEqual($unpaidBillLeft, $payingBillAmountLeft, static::precision))) {
					$overPayingBill->attachPaidBill($unpaidBill->getType(), $unpaidBill->getId(), $payingBillAmountLeft)->save();
					$unpaidBill->attachPayingBill($overPayingBill, $payingBillAmountLeft)->save();
					unset($unpaidBills[$key1]);
					unset($overPayingBills[$key2]);
					break;
				}
			}
		}
		foreach ($unpaidBills as $unpaidBillRaw) {
			$unpaidBill = Billrun_Bill::getInstanceByData($unpaidBillRaw);
			$unpaidBillLeft = $unpaidBill->getLeftToPay();
			foreach ($overPayingBills as $overPayingBill) {
				$payingBillAmountLeft = $overPayingBill->getLeft();
				if ($payingBillAmountLeft) {
					$amountPaid = min(array($unpaidBillLeft, $payingBillAmountLeft));
					$overPayingBill->attachPaidBill($unpaidBill->getType(), $unpaidBill->getId(), $amountPaid)->save();
					$unpaidBill->attachPayingBill($overPayingBill, $amountPaid)->save();
					$unpaidBillLeft -= $amountPaid;
				}
				if (abs($unpaidBillLeft) < static::precision) {
					break;
				}
			}
		}
	}

	public static function getUnpaidBills($query = array(), $sort = array()) {
		$mandatoryQuery = static::getUnpaidQuery();
		$query = array_merge($query, $mandatoryQuery);
		return static::getBills($query, $sort);
	}

	protected function updateLeft() {
		if ($this->getDue() < 0 && ($this->getBillMethod() != 'denial')) {
			$this->data['left'] = $this->getAmount();
			foreach ($this->getPaidBills() as $paidBills) {
				$this->data['left'] -= array_sum($paidBills);
			}
			if (abs($this->data['left']) < Billrun_Bill::precision) {
				$this->data['left'] = 0;
			}
		}
	}
		
	protected function updateLeftToPay() {
		if ($this->getDue() > 0 && ($this->getBillMethod() != 'denial')) {
			$this->data['left_to_pay'] = $this->getAmount();
			foreach ($this->getPaidByBills() as $paidByBill) {
				$this->data['left_to_pay'] -= floatval($paidByBill['amount']);
			}
			if ($this->data['left_to_pay'] < Billrun_Bill::precision) {
				$this->data['left_to_pay'] = 0;
			}
		}
	}

	public function getPaidByBills() {
		return isset($this->data['paid_by']) ? $this->data['paid_by'] : array();
	}

	/**
	 * Get bills awaiting to be paid
	 * @return array
	 */
	public static function getUnpaidQuery() {
		return array_merge(array('due' => array('$gt' => 0,), 'left_to_pay' => array('$gt' => 0), 'paid' => array('$nin' => array(TRUE, '1', '2'),),), static::getNotRejectedOrCancelledQuery()
		);
	}

	public static function getBills($query = array(), $sort = array()) {
		$billsColl = Billrun_Factory::db()->billsCollection();
		return iterator_to_array($billsColl->find($query)->sort($sort), FALSE);
	}

	public static function getOverPayingBills($query = array(), $sort = array()) {
		$billObjs = array();
		$query = array_merge($query, array('left' => array('$gt' => 0,)), static::getNotRejectedOrCancelledQuery());
		$bills = static::getBills($query, $sort);
		if ($bills) {
			foreach ($bills as $bill) {
				$billObjs[] = static::getInstanceByData($bill);
			}
		}
		return $billObjs;
	}

	/**
	 * 
	 * @param Mongodloid_Entity|array $data
	 * @return Billrun_Bill
	 */
	public static function getInstanceByData($data) {
		if ($data['type'] == 'inv') {
			return Billrun_Bill_Invoice::getInstanceByData($data);
		} else if ($data['type'] == 'rec') {
			return Billrun_Bill_Payment::getInstanceByData($data);
		} else {
			throw new Exception('Unknown bill type');
		}
	}

	public function reduceLeft($byAmount = 0) {
		if ($byAmount) {
			$left = $this->getLeft();
			if ($left >= $byAmount) {
				$this->data['left'] = $left - $byAmount;
			}
		}
		return $this;
	}

	public function getLeft() {
		return isset($this->data['left']) ? $this->data['left'] : 0;
	}

	public function detachPaidBills() {
		foreach ($this->getPaidBills() as $bill) {
			$billObj = Billrun_Bill::getInstanceByTypeAndid($bill['type'], $bill['id']);
			$billObj->detachPayingBill($this->getType(), $this->getId())->save();
		}
	}
	
	public function detachPayingBills() {
		foreach ($this->getPaidByBills() as $bill) {
			$billObj = Billrun_Bill::getInstanceByTypeAndid($bill['type'], $bill['id']);
			$billObj->detachPaidBill($this->getType(), $this->getId())->save();
		}
	}

	public function getType() {
		return $this->type;
	}

	public function getPaidAmount() {
		return isset($this->data['total_paid']) ? $this->data['total_paid'] : 0;
	}

	protected function recalculatePaymentFields($billId = null, $status = null) {
		if ($this->getBillMethod() == 'denial') {
			return $this;
		}
		if ($this->getDue() > 0) {
			$amount = 0;
			foreach (Billrun_Util::getIn($this->data, 'paid_by', []) as $relatedBill) {
				$amount += floatval($relatedBill['amount']);
			}
			
			$this->data['total_paid'] = $amount;
			$this->data['left_to_pay'] = round($this->getLeftToPay(), 2);
			$this->data['vatable_left_to_pay'] = min($this->getLeftToPay(), $this->getDueBeforeVat());
			if (is_null($status)){
				$this->data['paid'] = $this->isPaid();
			} else {
				$this->data['paid'] = $this->calcPaidStatus($billId, $status);
			}
		} else if ($this->getDue() < 0){
			$amount = 0;
			foreach (Billrun_Util::getIn($this->data, 'pays', []) as $relatedBill) {
				$amount += floatval($relatedBill['amount']);
			}
			
			$this->data['left'] = round($this->data['amount'] - $amount, 2);				
		}
		return $this;
	}

	/**
	 * 
	 * @param int $id
	 * @return Billrun_Bill_Invoice
	 */
	public static function getInstanceByTypeAndid($type, $id) {
		if ($type == 'inv') {
			return Billrun_Bill_Invoice::getInstanceByid($id);
		} else if ($type == 'rec') {
			return Billrun_Bill_Payment::getInstanceByid($id);
		}
		throw new Exception('Unknown bill type');
	}

	public function attachPayingBill($bill, $amount, $status = null) {
		$billId = $bill->getId();
		$billType = $bill->getType();
		if ($amount) {
			$paidBy = $this->getPaidByBills();
			$relatedBillId = Billrun_Bill::findRelatedBill($paidBy, $billType, $billId);
			if ($relatedBillId == -1) {
				Billrun_Bill::addRelatedBill($paidBy, $billType, $billId, $amount);
			} else {
				$paidBy[$relatedBillId]['amount'] += floatval($amount);
			}
			
			if ($bill->isPendingPayment()) {
				$this->addToWaitingPayments($billId, $billType);
			}
			if ($status == 'Rejected') {
				$this->addToRejectedPayments($billId, $billType);
			}
			$this->updatePaidBy($paidBy, $billId, $status);
		}
		return $this;
	}

	public function detachPayingBill($billType, $id) {
		$paidBy = $this->getPaidByBills();
		$index = Billrun_Bill::findRelatedBill($paidBy, $billType, $id);
		if ($index > -1) {
			unset($paidBy[$index]);
			$this->updatePaidBy(array_values($paidBy));
		}
		return $this;
	}
	
	public function detachPaidBill($billType, $id) {
		$pays = $this->getPaidBills();
		$index = Billrun_Bill::findRelatedBill($pays, $billType, $id);
		if ($index > -1) {
			unset($pays[$index]);
			$this->updatePays(array_values($pays));
		}
		return $this;
	}

	protected function updatePaidBy($paidBy, $billId = null, $status = null) {
		if ($this->getDue() > 0 || $this->isRejection() || $this->isCancellation()) {
			$this->data['paid_by'] = $paidBy;
			$this->recalculatePaymentFields($billId, $status);
		}
	}
	
	protected function updatePays($pays, $billId = null) {
		if ($this->getDue() < 0) {
			$this->data['pays'] = $pays;
			$this->recalculatePaymentFields();
		}
	}

	public function attachPaidBill($billType, $billId, $amount) {
		$paymentRawData = $this->data->getRawData();
		$relatedBillId = Billrun_Bill::findRelatedBill($paymentRawData['pays'], $billType, $billId);
		if ($relatedBillId == -1) {
			Billrun_Bill::addRelatedBill($paymentRawData['pays'], $billType, $billId, $amount);
		} else {
			$paidBy[$relatedBillId]['amount'] += floatval($amount);
		}
		$this->data->setRawData($paymentRawData);
		$this->updateLeft();
		return $this;
	}

	public function getPaidBills() {
		return isset($this->data['pays']) ? $this->data['pays'] : array();<commit_msg>BRCD-2452: fix deposit's "left" value<commit_after><?php

/**
 * @package         Billing
 * @copyright       Copyright (C) 2012-2016 BillRun Technologies Ltd. All rights reserved.
 * @license         GNU Affero General Public License Version 3; see LICENSE.txt
 */

/**
 * BillRun Bill class
 *
 * @package  Billrun
 * @since    5.0
 */
abstract class Billrun_Bill {

	/**
	 *
	 * @var string
	 */
	protected $type;

	/**
	 *
	 * @var Mongodloid_Entity
	 */
	protected $data;

	/**
	 *
	 * @var Mongodloid_Collection
	 */
	protected $billsColl;

	/**
	 * Optional fields to be saved to the payment. For some payment methods they are mandatory.
	 * @var array
	 */
	protected $optionalFields = array();

	const precision = 0.00001;
	
	/**
	 * 
	 * @param type $options
	 */
	public function __construct($options) {
		$this->updateLeft();
		$this->updateLeftToPay();
		$this->recalculatePaymentFields();
	}

	public function getRawData() {
		return $this->data->getRawData();
	}

	/**
	 * 
	 * @param array $rawData
	 */
	protected function setRawData($rawData) {
		$this->data = new Mongodloid_Entity($rawData, $this->billsColl);
	}

	public function getBIC() {
		if (isset($this->data['BIC'])) {
			return $this->data['BIC'];
		}
		return NULL;
	}

	public function getAmount() {
		if (isset($this->data['amount'])) {
			return $this->data['amount'];
		}
		return NULL;
	}

	public function getDue() {
		if (isset($this->data['due'])) {
			return $this->data['due'];
		}
		return 0;
	}

	public function getTime() {
		if (isset($this->data['urt'])) {
			return $this->data['urt'];
		}
		return NULL;
	}

	public function getAccountNo() {
		if (isset($this->data['aid'])) {
			return $this->data['aid'];
		}
		return NULL;
	}

	public function getIBAN() {
		if (isset($this->data['IBAN'])) {
			return $this->data['IBAN'];
		}
		return NULL;
	}

	public function getRUM() {
		if (isset($this->data['RUM'])) {
			return $this->data['RUM'];
		}
		return NULL;
	}

	public function getBankName() {
		if (isset($this->data['bank_name'])) {
			return $this->data['bank_name'];
		}
		return NULL;
	}

	public function getCustomerName() {
		if (isset($this->data['payer_name'])) {
			return $this->data['payer_name'];
		}
		return NULL;
	}

	public function getCustomerAddress() {
		if (isset($this->data['aaddress'])) {
			return $this->data['aaddress'];
		}
		return NULL;
	}

	public function getCustomerZipCode() {
		if (isset($this->data['azip'])) {
			return $this->data['azip'];
		}
		return NULL;
	}

	public function getCustomerCity() {
		if (isset($this->data['acity'])) {
			return $this->data['acity'];
		}
		return NULL;
	}

	public function getCustomerBankName() {
		if (isset($this->data['bank_name'])) {
			return $this->data['bank_name'];
		}
		return NULL;
	}

	public function save() {
		$this->data->save(NULL, 1);
		return true;
	}

	abstract public function getId();

	public static function getTotalDue($query = array(), $minBalance = false, $notFormatted = false) {
		$billsColl = Billrun_Factory::db()->billsCollection();
		$query = array(
			'$match' => $query,
		);
		$project1 = array(
			'$project' => array(
				'aid' => 1,
				'waiting_for_confirmation' => 1,
				'due' => 1,
				'waiting_for_confirmation_total' => array('$cond' => array('if' => array('$ne' => array('$waiting_for_confirmation', true)), 'then' => '$due' , 'else' => 0)),
				'pending_total' => array('$cond' => array('if' => array('$eq' => array('$pending', true)), 'then' => '$due' , 'else' => 0)),

			),
		);
		$group = array(
			'$group' => array(
				'_id' => '$aid',
				'total' => array(
					'$sum' => '$due',
				),
				'waiting_for_confirmation_total' => array(
					'$sum' => '$waiting_for_confirmation_total',
				),
				'pending_total' => array(
					'$sum' => '$pending_total',
				),
			),
		);
		
		$project2 = array(
			'$project' => array(
				'_id' => 0,
				'aid' => '$_id',
				'total' => 1,
				'waiting_for_confirmation_total' =>  1,
				'pending_total' => 1
			),
		);

		$having = array('$match' => array());
		if ($minBalance !== FALSE) {
			$having['$match']['total'] = array(
				'$gte' => $minBalance,
			);
		}
		if ($having['$match']) {
			$results = $billsColl->aggregate($query, $project1, $group, $project2, $having);
		} else {
			$results = $billsColl->aggregate($query,$project1, $group, $project2);
		}
		
		$results = iterator_to_array($results);
		if (!$notFormatted) {
			$results = array_map(function($ele) {
				$ele['total'] = Billrun_Util::getChargableAmount($ele['total']);
				$ele['waiting_for_confirmation_total'] = Billrun_Util::getChargableAmount($ele['waiting_for_confirmation_total']);
				$ele['pending_total'] = Billrun_Util::getChargableAmount($ele['pending_total']);
				return $ele;
			}, $results);
		}
		return array_combine(array_map(function($ele) {
				return $ele['aid'];
			}, $results), $results);
	}

	/**
	 * Return total / total waiting due for account
	 * @param int $aid
	 * @param boolean $notFormatted
	 * @return array
	 */
	public static function getTotalDueForAccount($aid, $date = null, $notFormatted = false) {
		$query = array('aid' => $aid);
		if (!empty($date)) {
			$query['$or'] = array(
				array('charge.not_before' => array('$lte' => new MongoDate(strtotime($date)))),
				array('charge.not_before' => array('$exists' => false)),
			);
		}
		$results = static::getTotalDue($query, $notFormatted);
		if (count($results)) {
			$total =  current($results)['total'];
			$totalWaiting = current($results)['waiting_for_confirmation_total'];
			$totalPending = abs(current($results)['pending_total']);
		} else if ($notFormatted) {
			$total = $totalWaiting = $totalPending = 0;
		} else {
			$total = $totalWaiting = $totalPending = Billrun_Util::getChargableAmount(0);
		}
		return array('total' => $total, 'without_waiting' => $totalWaiting, 'total_pending_amount' => $totalPending);
	}

	public static function payUnpaidBillsByOverPayingBills($aid, $sortByUrt = true) {
		$query = array(
			'aid' => $aid,
		);
		$sort = array(
			'urt' => 1,
		);
		$unpaidBills = Billrun_Bill::getUnpaidBills($query, $sort);
		$overPayingBills = Billrun_Bill::getOverPayingBills($query, $sort);
		foreach ($unpaidBills as $key1 => $unpaidBillRaw) {
			$unpaidBill = Billrun_Bill::getInstanceByData($unpaidBillRaw);
			$unpaidBillLeft = $unpaidBill->getLeftToPay();
			foreach ($overPayingBills as $key2 => $overPayingBill) {
				$payingBillAmountLeft = $overPayingBill->getLeft();
				if ($payingBillAmountLeft && (Billrun_Util::isEqual($unpaidBillLeft, $payingBillAmountLeft, static::precision))) {
					$overPayingBill->attachPaidBill($unpaidBill->getType(), $unpaidBill->getId(), $payingBillAmountLeft)->save();
					$unpaidBill->attachPayingBill($overPayingBill, $payingBillAmountLeft)->save();
					unset($unpaidBills[$key1]);
					unset($overPayingBills[$key2]);
					break;
				}
			}
		}
		foreach ($unpaidBills as $unpaidBillRaw) {
			$unpaidBill = Billrun_Bill::getInstanceByData($unpaidBillRaw);
			$unpaidBillLeft = $unpaidBill->getLeftToPay();
			foreach ($overPayingBills as $overPayingBill) {
				$payingBillAmountLeft = $overPayingBill->getLeft();
				if ($payingBillAmountLeft) {
					$amountPaid = min(array($unpaidBillLeft, $payingBillAmountLeft));
					$overPayingBill->attachPaidBill($unpaidBill->getType(), $unpaidBill->getId(), $amountPaid)->save();
					$unpaidBill->attachPayingBill($overPayingBill, $amountPaid)->save();
					$unpaidBillLeft -= $amountPaid;
				}
				if (abs($unpaidBillLeft) < static::precision) {
					break;
				}
			}
		}
	}

	public static function getUnpaidBills($query = array(), $sort = array()) {
		$mandatoryQuery = static::getUnpaidQuery();
		$query = array_merge($query, $mandatoryQuery);
		return static::getBills($query, $sort);
	}

	protected function updateLeft() {
		if ($this->getDue() < 0 && ($this->getBillMethod() != 'denial')) {
			$this->data['left'] = $this->getAmount();
			foreach ($this->getPaidBills() as $paidBill) {
				$this->data['left'] -= $paidBill['amount'];
			}
			if (abs($this->data['left']) < Billrun_Bill::precision) {
				$this->data['left'] = 0;
			}
		}
	}
		
	protected function updateLeftToPay() {
		if ($this->getDue() > 0 && ($this->getBillMethod() != 'denial')) {
			$this->data['left_to_pay'] = $this->getAmount();
			foreach ($this->getPaidByBills() as $paidByBill) {
				$this->data['left_to_pay'] -= floatval($paidByBill['amount']);
			}
			if ($this->data['left_to_pay'] < Billrun_Bill::precision) {
				$this->data['left_to_pay'] = 0;
			}
		}
	}

	public function getPaidByBills() {
		return isset($this->data['paid_by']) ? $this->data['paid_by'] : array();
	}

	/**
	 * Get bills awaiting to be paid
	 * @return array
	 */
	public static function getUnpaidQuery() {
		return array_merge(array('due' => array('$gt' => 0,), 'left_to_pay' => array('$gt' => 0), 'paid' => array('$nin' => array(TRUE, '1', '2'),),), static::getNotRejectedOrCancelledQuery()
		);
	}

	public static function getBills($query = array(), $sort = array()) {
		$billsColl = Billrun_Factory::db()->billsCollection();
		return iterator_to_array($billsColl->find($query)->sort($sort), FALSE);
	}

	public static function getOverPayingBills($query = array(), $sort = array()) {
		$billObjs = array();
		$query = array_merge($query, array('left' => array('$gt' => 0,)), static::getNotRejectedOrCancelledQuery());
		$bills = static::getBills($query, $sort);
		if ($bills) {
			foreach ($bills as $bill) {
				$billObjs[] = static::getInstanceByData($bill);
			}
		}
		return $billObjs;
	}

	/**
	 * 
	 * @param Mongodloid_Entity|array $data
	 * @return Billrun_Bill
	 */
	public static function getInstanceByData($data) {
		if ($data['type'] == 'inv') {
			return Billrun_Bill_Invoice::getInstanceByData($data);
		} else if ($data['type'] == 'rec') {
			return Billrun_Bill_Payment::getInstanceByData($data);
		} else {
			throw new Exception('Unknown bill type');
		}
	}

	public function reduceLeft($byAmount = 0) {
		if ($byAmount) {
			$left = $this->getLeft();
			if ($left >= $byAmount) {
				$this->data['left'] = $left - $byAmount;
			}
		}
		return $this;
	}

	public function getLeft() {
		return isset($this->data['left']) ? $this->data['left'] : 0;
	}

	public function detachPaidBills() {
		foreach ($this->getPaidBills() as $bill) {
			$billObj = Billrun_Bill::getInstanceByTypeAndid($bill['type'], $bill['id']);
			$billObj->detachPayingBill($this->getType(), $this->getId())->save();
		}
	}
	
	public function detachPayingBills() {
		foreach ($this->getPaidByBills() as $bill) {
			$billObj = Billrun_Bill::getInstanceByTypeAndid($bill['type'], $bill['id']);
			$billObj->detachPaidBill($this->getType(), $this->getId())->save();
		}
	}

	public function getType() {
		return $this->type;
	}

	public function getPaidAmount() {
		return isset($this->data['total_paid']) ? $this->data['total_paid'] : 0;
	}

	protected function recalculatePaymentFields($billId = null, $status = null) {
		if ($this->getBillMethod() == 'denial') {
			return $this;
		}
		if ($this->getDue() > 0) {
			$amount = 0;
			foreach (Billrun_Util::getIn($this->data, 'paid_by', []) as $relatedBill) {
				$amount += floatval($relatedBill['amount']);
			}
			
			$this->data['total_paid'] = $amount;
			$this->data['left_to_pay'] = round($this->getLeftToPay(), 2);
			$this->data['vatable_left_to_pay'] = min($this->getLeftToPay(), $this->getDueBeforeVat());
			if (is_null($status)){
				$this->data['paid'] = $this->isPaid();
			} else {
				$this->data['paid'] = $this->calcPaidStatus($billId, $status);
			}
		} else if ($this->getDue() < 0){
			$amount = 0;
			foreach (Billrun_Util::getIn($this->data, 'pays', []) as $relatedBill) {
				$amount += floatval($relatedBill['amount']);
			}
			
			$this->data['left'] = round($this->data['amount'] - $amount, 2);				
		}
		return $this;
	}

	/**
	 * 
	 * @param int $id
	 * @return Billrun_Bill_Invoice
	 */
	public static function getInstanceByTypeAndid($type, $id) {
		if ($type == 'inv') {
			return Billrun_Bill_Invoice::getInstanceByid($id);
		} else if ($type == 'rec') {
			return Billrun_Bill_Payment::getInstanceByid($id);
		}
		throw new Exception('Unknown bill type');
	}

	public function attachPayingBill($bill, $amount, $status = null) {
		$billId = $bill->getId();
		$billType = $bill->getType();
		if ($amount) {
			$paidBy = $this->getPaidByBills();
			$relatedBillId = Billrun_Bill::findRelatedBill($paidBy, $billType, $billId);
			if ($relatedBillId == -1) {
				Billrun_Bill::addRelatedBill($paidBy, $billType, $billId, $amount);
			} else {
				$paidBy[$relatedBillId]['amount'] += floatval($amount);
			}
			
			if ($bill->isPendingPayment()) {
				$this->addToWaitingPayments($billId, $billType);
			}
			if ($status == 'Rejected') {
				$this->addToRejectedPayments($billId, $billType);
			}
			$this->updatePaidBy($paidBy, $billId, $status);
		}
		return $this;
	}

	public function detachPayingBill($billType, $id) {
		$paidBy = $this->getPaidByBills();
		$index = Billrun_Bill::findRelatedBill($paidBy, $billType, $id);
		if ($index > -1) {
			unset($paidBy[$index]);
			$this->updatePaidBy(array_values($paidBy));
		}
		return $this;
	}
	
	public function detachPaidBill($billType, $id) {
		$pays = $this->getPaidBills();
		$index = Billrun_Bill::findRelatedBill($pays, $billType, $id);
		if ($index > -1) {
			unset($pays[$index]);
			$this->updatePays(array_values($pays));
		}
		return $this;
	}

	protected function updatePaidBy($paidBy, $billId = null, $status = null) {
		if ($this->getDue() > 0 || $this->isRejection() || $this->isCancellation()) {
			$this->data['paid_by'] = $paidBy;
			$this->recalculatePaymentFields($billId, $status);
		}
	}
	
	protected function updatePays($pays, $billId = null) {
		if ($this->getDue() < 0) {
			$this->data['pays'] = $pays;
			$this->recalculatePaymentFields();
		}
	}

	public function attachPaidBill($billType, $billId, $amount) {
		$paymentRawData = $this->data->getRawData();
		$relatedBillId = Billrun_Bill::findRelatedBill($paymentRawData['pays'], $billType, $billId);
		if ($relatedBillId == -1) {
			Billrun_Bill::addRelatedBill($paymentRawData['pays'], $billType, $billId, $amount);
		} else {
			$paymentRawData['pays'][$relatedBillId]['amount'] += floatval($amount);
		}
		$this->data->setRawData($paymentRawData);
		$this->updateLeft();
		return $this;
	}

	public function getPaidBills() {
		return isset($this->data['pays']) ? $this->data['pays'] : array();
{'fraction_non_alphanumeric': 0.19501267669225159, 'fraction_numerical': 0.0046539089361997705, 'mean_word_length': 4.254379562043796, 'pattern_counts': {'":': 0, '<': 17, '<?xml version=': 0, '>': 391, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2816538', 'n_tokens_mistral': 11622, 'n_tokens_neox': 10086, 'n_words': 1843}