<?php
/**
 * Class represents records from table aff_commission_rule
 * {autogenerated}
 * @property int $rule_id 
 * @property string $comment 
 * @property int $sort_order 
 * @property mixed $conditions
 * @property double $free_signup_c 
 * @property string $free_signup_t 
 * @property double $first_payment_c 
 * @property string $first_payment_t 
 * @property double $recurring_c 
 * @property string $recurring_t 
 * @property int $type
 * @property int $tier
 * @property float $multi
 * @property bool $is_disabled
 * @see Am_Table
 */
class AffCommissionRule extends Am_Record 
{
    const TYPE_MULTI    = 'multi';
    const TYPE_CUSTOM   = 'custom';
    const TYPE_GLOBAL   = 'global';
    
    const COND_AFF_SALES_COUNT = 'aff_sales_count';
    const COND_AFF_SALES_AMOUNT = 'aff_sales_amount';
    const COND_AFF_GROUP_ID    = 'aff_group_id';
    const COND_PRODUCT_ID      = 'product_id';
    const COND_PRODUCT_CATEGORY_ID = 'product_category_id';
    const COND_COUPON = 'coupon';
    
    static function getTypes()
    {
        return array(
            self::TYPE_CUSTOM   => ___("Custom Commission"),
            self::TYPE_MULTI    => ___("Multiplier"),
        );
    }

    function getTypeTitle()
    {
        $level = $this->tier + 1;
        $types = self::getTypes();
        
        return $this->type == self::TYPE_GLOBAL ? ($this->tier > 0 ? ___('%d-Tier Affiliates Commission
', $level) : ___('Global Commission')) : $types[$this->type];
    }
    
    function isGlobal()
    {
        if (empty($this->type)) return false;
        return self::isGlobalType($this->type);
    }
    static function isGlobalType($type)
    {
        return $type == self::TYPE_GLOBAL;
    }
    function match(Invoice $invoice, InvoiceItem $item, User $aff, $paymentNumber = 0, $tier = 0, $paymentDate = 'now')
    {
        if ($this->type == self::TYPE_GLOBAL)
            return $tier == $this->tier;

        if ($tier != 0) return false; // no custom rules for 2-tier

        // check conditions
        foreach ($this->getConditions() as $conditionType => $vars)
        {
            switch ($conditionType)
            {
                case self::COND_AFF_SALES_COUNT:;
                case self::COND_AFF_SALES_AMOUNT:;
                    if (empty($vars['count']) || empty($vars['days'])) return false;
                    $e = sqlDate($paymentDate);
                    $b = sqlDate($e . '-' . $vars['days'] . ' days');
                    $stats = $this->getDi()->affCommissionTable->getAffStats($aff->pk(), $b, $e);
                    $key = $conditionType == self::COND_AFF_SALES_AMOUNT ? 'amount' : 'count';
                    if ($stats[$key] < $vars['count']) 
                        return false;
                    break;
                case self::COND_AFF_GROUP_ID:
                    if (!array_intersect($aff->getGroups(), (array)$vars))
                        return false;
                    break;
                case self::COND_PRODUCT_ID:
                    if (($item->item_type != 'product') || !in_array($item->item_id, (array)$vars))
                        return false;
                    break;
                case self::COND_PRODUCT_CATEGORY_ID:
                    if ($item->item_type != 'product') return false;
                    $pr = $item->tryLoadProduct();
                    if (!$pr) return false;
                    if (!array_intersect($pr->getCategories(), (array)$vars))
                        return false;
                    break;
                case self::COND_COUPON:
                    $coupon = $invoice->getCoupon();
                    switch ($vars['type']) {
                        case 'any' :
                            $coupon_cond_match = (bool)$coupon;
                            break;
                        case 'coupon':
                            $coupon_cond_match = $coupon && ($vars['code'] == $coupon->code);
                            break;
                        case 'batch' :
                            $coupon_cond_match = $coupon && ($vars['batch_id'] == $coupon->batch_id);
                            break;
                    }
                    if ($vars['used'] ? !$coupon_cond_match : $coupon_cond_match)
                        return false;
                    break;
                default: 
                    return false;
            }
        }
        return true;
    }
    function getConditions()
    {
        return (array)json_decode($this->conditions, true);
    }
    function setConditions(array $conditions)
    {
        $this->conditions = json_encode($conditions);
        return $this;
    }
    function render($pad = "")
    {
        $condition = $this->renderConditions();
        if ($condition) $condition = $pad . "  conditions: " . $condition. "\n";
        return 
            sprintf("%s%s (%s)\n", $pad,$this->comment, $this->renderCommission()).
            $condition;
    }
    function renderConditions()
    {
        $ret = array();
        foreach ($this->getConditions() as $conditionType => $vars)
        {
            switch ($conditionType)
            {
                case self::COND_AFF_SALES_COUNT:;
                    $ret[] = sprintf("affiliate generated %d sales last %d days", 
                        $vars['count'], $vars['days']);
                case self::COND_AFF_SALES_AMOUNT:;
                    $ret[] = sprintf("affiliate generated %d%s in commissions last %d days", 
                        $vars['count'], Am_Currency::getDefault(), $vars['days']);
                    break;
                case self::COND_AFF_GROUP_ID:
                    $v = array();
                    foreach ($this->getDi()->userGroupTable->loadIds((array)$vars) as $group)
                        $v[] = $group->title;
                    $ret[] = sprintf("affiliate group IN (%s)", implode(", ", $v));
                    break;
                case self::COND_PRODUCT_ID:
                    $v = array();
                    foreach ($this->getDi()->productTable->loadIds((array)$vars) as $product)
                        $v[] = $product->title;
                    $ret[] = sprintf("products IN (%s)", implode(", ", $v));
                    break;
                case self::COND_PRODUCT_CATEGORY_ID:
                    $v = array();
                    foreach ($this->getDi()->productCategoryTable->loadIds((array)$vars) as $product)
                        $v[] = $product->title;
                    $ret[] = sprintf("product category IN (%s)", implode(", ", $v));
                    break;
                case self::COND_COUPON:
                    $end = '';
                    switch ($vars['type']) {
                            case 'any' :
                                $end = 'Any Coupon';
                                break;
                            case 'coupon' :
                                $end = 'Coupon with Code : ' . $vars['code'];
                                break;
                            case 'batch' :
                                $options = $this->getDi()->couponBatchTable->getOptions(false);
                                $end = 'Coupon from Batch : ' . $options[$vars['batch_id']];
                                break;
                        }
                    $ret[] = ($vars['used'] ? 'Used ' : "Did't Use ") . $end;
                    break;
                default: 
                    return false;
            }
        }
        return implode(" AND ", $ret);
    }
    /**
     * Return human-readable representation of commission
     */
    function renderCommission()
    {
        $text = "";
        if ($this->type == AffCommissionRule::TYPE_MULTI)
            return ___('commission found by next rules') . '&times; ' . $this->multi;
        if ($this->type == AffCommissionRule::TYPE_GLOBAL && $this->tier>0)
            return $this->first_payment_c . '%';
        foreach (array('first_payment' => 'First Payment', 'recurring' => 'Second and Subsequent Payments', 'free_signup' => 'Free Signup') as $fieldName => $label)
        {
            if ($this->get($fieldName . '_c') <= 0) continue;
            $v = $this->get($fieldName . '_c') . $this->get($fieldName . '_t');
            $text .= " " . $label . ":" . $v . " ";
        }
        return $text;
    }
}

class AffCommissionRuleTable extends Am_Table 
{
    protected $_key = 'rule_id';
    protected $_table = '?_aff_commission_rule';
    protected $_recordClass = 'AffCommissionRule';
    
    protected $rulesCache = array();
    
    /**
     * @return Am_Query with correct order set
     */
    public function createQuery()
    {
        $q = new Am_Query($this);
        $q->setOrderRaw("IF(type='multi', 1, type+0), sort_order");
        return $q;
    }
    public function _resetCache()
    {
        $this->rulesCache = array();
    }
    
    /**
     * @param Invoice $invoice
     * @param int $paymentNumber number of payment for given invoice, staring from 0
     * @param int $tier Affiliate level - starting from 0
     * @return array<AffCommissionRule> matching the invoice
     */
    public function findRules(Invoice $invoice, InvoiceItem $item, User $aff, $paymentNumber = 0, $tier = 0, $paymentDate = 'now')
    {
        if (!$this->rulesCache)
        {
            $this->rulesCache = $this->createQuery()->selectPageRecords(0, 99999);
        }
        $ret = array();
        foreach ($this->rulesCache as $rule)
        {
            /* @var $rule AffCommissionRule */
            if ($rule->match($invoice, $item, $aff, $paymentNumber, $tier, $paymentDate)) 
            {
                $ret[] = $rule;
                if($rule->type == AffCommissionRule::TYPE_GLOBAL && $rule->tier > 0) $rule->first_payment_t = '%';
                if ($rule->type != AffCommissionRule::TYPE_MULTI)  
                    break; // last rule
            }
        }
        return $ret;
    }
    public function calculate(Invoice $invoice, InvoiceItem $item, User $aff, $paymentNumber = 0, $tier = 0, $paymentAmount = 0.0, $paymentDate = 'now')
    {
        // take aff.commission_days in account for 1-tier only
        if ($tier == 0 && ($commissionDays = $this->getDi()->config->get('aff.commission_days')))
        {
            $signupDays = $this->getDi()->time - strtotime($invoice->getUser()->added);
            $signupDays = intval($signupDays / (3600*24)); // to days
            if ($commissionDays < $signupDays)
                return; // no commission for this case, affiliate<->user relation is expired
        }
        
        $multi = 1.0;
        $isFirst = $paymentNumber<=1;
        $prefix = $isFirst && (float)$item->first_total ? 'first' : 'second';

        if ($tier == 0) // for tier 0 get amount paid for given item
        {
            if ($invoice->get("{$prefix}_total") == 0)
                $paidForItem = 0; // avoid division by zero
            else
                $paidForItem = $paymentAmount * $item->get("{$prefix}_total") / $invoice->get("{$prefix}_total");
        } else { // for higher tier just take amount paid to previous tier
            $paidForItem = $paymentAmount;
        }
        foreach ($this->findRules($invoice, $item, $aff, $paymentNumber, $tier, $paymentDate) as $rule)
        {
            // Second tier commission have to be calculated as percent from First tier commission. 
            if ($tier > 0)
                return moneyRound($rule->first_payment_c * $paidForItem / 100);

            if ($rule->type == AffCommissionRule::TYPE_MULTI)
                $multi *= $rule->multi;
            else {
                if ($paidForItem == 0)
                {
                    // free signup?
                    if (($paymentNumber == 0) && $rule->free_signup_c) return moneyRound($multi * $rule->free_signup_c);
                } elseif ($isFirst) {
                    // first payment
                    if ($rule->first_payment_t == '%')
                        return moneyRound($multi * $rule->first_payment_c * $paidForItem / 100);
                    else
                        return moneyRound($multi * $rule->first_payment_c);
                } else {
                    // recurring payment
                    if ($rule->recurring_t == '%')
                        return moneyRound($multi * $rule->recurring_c * $paidForItem / 100);
                    else
                        return moneyRound($multi * $rule->recurring_c);
                }
            }
        }
    }

    public function getMaxTier()
    {
        return $this->getDi()->db->selectCell("SELECT MAX(tier) FROM ?_aff_commission_rule");
    }
    
    /**
     * Process invoice and insert necessary commissions for it
     *
     * External code should guarantee that this method with $payment = null will be called
     * only once for each user for First user invoice
     */
    public function processPayment(Invoice $invoice, InvoicePayment $payment = null)
    {
        $user = $invoice->getUser();
        if (empty($user->aff_id)) return ; // no affiliate id registered
        $aff  = $this->getDi()->userTable->load($user->aff_id, false);
        if (!$aff || !$aff->is_affiliate) return; // affiliate not found
        // try to load other tier affiliate
        $aff_tier = $aff;
        $aff_tiers = array();
        for ($tier=1; $tier<=$this->getMaxTier(); $tier++) {
            if (!$aff_tier->aff_id)
                break;

            $aff_tier = $this->getDi()->userTable->load($aff_tier->aff_id, false);
            if (!$aff_tier || !$aff_tier->is_affiliate)
                break;

            $aff_tiers[$tier] = $aff_tier;
        }

        $isFirst = !$payment || $payment->isFirst(); //to define price field
        $paymentNumber = is_null($payment) ? 0 : $invoice->getPaymentsCount();
        if (!$payment)
            $tax = 0;
        else
            $tax = $this->getDi()->config->get('aff.commission_include_tax', false) ? 0 : doubleval($payment->tax);
        $amount  = $payment ? ($payment->amount - $tax): 0;
        $date    = $payment ? $payment->dattm : 'now';
        // now calculate commissions
        $items = is_null($payment) ? array_slice($invoice->getItems(), 0, 1) : $invoice->getItems();
        foreach ($items as $item)
        {

            //we do not calculate commission for free items in invoice
            $prefix = $isFirst ? 'first' : 'second';
            if (!is_null($payment) && !(float)$item->get("{$prefix}_total")) continue;

            $comm = $this->getDi()->affCommissionRecord;
            $comm->date  = sqlDate($date);
            $comm->record_type = AffCommission::COMMISSION;
            $comm->invoice_id = $invoice->invoice_id;
            $comm->invoice_payment_id  = $payment ? $payment->pk() : null;
            $comm->receipt_id  = $payment ? $payment->receipt_id : null;
            $comm->product_id  = $item->item_id;
            $comm->is_first    =  $paymentNumber<=1;

            $comm->_setPayment($payment);
            $comm->_setInvoice($invoice);
            $comm_tier = clone $comm;

            $topay_this = $topay = $this->calculate($invoice, $item, $aff, $paymentNumber, 0, $amount, $date);
            if ($topay)
            {
                $comm->aff_id = $aff->pk();
                $comm->amount = $topay;
                $comm->tier = 0;
                $comm->_setAff($aff);
                $comm->insert();
            }

            foreach ($aff_tiers as $tier => $aff_tier) {
                $topay_this = $this->calculate($invoice, $item, $aff_tier, $paymentNumber, $tier, $topay_this, $date);
                if ($topay_this)
                {
                    $comm_this = clone $comm_tier;
                    $comm_this->aff_id = $aff_tier->pk();
                    $comm_this->amount = $topay_this;
                    $comm_this->tier = $tier;
                    $comm_this->_setAff($aff_tier);
                    $comm_this->insert();
                }
            }
        }
    }
    
    /**
     * Process refund to rollback existing commissions
     */
    public function processRefund(Invoice $invoice, InvoiceRefund $refund)
    {
        if ($refund->invoice_payment_id)
            $toRefund = $this->getDi()->affCommissionTable->findByInvoicePaymentId($refund->invoice_payment_id);
        else
            $toRefund = $this->getDi()->affCommissionTable->findLastRecordsByInvoiceId($refund->invoice_id);
        foreach ($toRefund as $affCommission)
        {
            $void = $this->getDi()->affCommissionRecord;
            $void->fromRow($affCommission->toRow());
            $void->date = $refund ? $refund->dattm : sqlDate('now');
            $void->commission_id = null;
            $void->record_type = AffCommission::VOID;
            try {
                $void->insert();
            } catch (Am_Exception_Db_NotUnique $e) {
                // already handled? keep silence
            }
        }
    }
    
    public function hasCustomRules()
    {
        return $this->_db->selectCell("SELECT COUNT(*) FROM $this->_table 
            WHERE `type` NOT IN (?a)", 
            array(AffCommissionRule::TYPE_GLOBAL,));
    }
}
