销售风控信用额度控制合同审批和订单审批的漏洞

第一 业务需求

销售信用额度风控逻辑存在漏洞,可能导致给客户超信用额度发货

原因:订单通过审批,发货,占用信用额度是有时间跨度的。如果在占用信用额度前可以提交任意合同和订单的审批,无法受到信用额度的限制。

方案:审批限制条件实际使用的信用额度改为锁定预期信用额度,把已发起审批和通过审批的订单都算入锁定预期信用额度

第二 产品设计

第三 系统分析

1.销售业务流程图

2.分析:

(1)获取客户的总信用额度,客户的总信用额度为客户申请信用后的总信用额度;可从客户基本表中获取。

(2)获取此客户的所有订单。

(3)轮询此客户的所有订单中已提交钉钉审批的订单,取订单金额为锁定信用。

(4)轮询此客户的所有订单中审批中的订单,取订单金额为锁定信用。

(5)轮询此客户的所有订单中已审批通过但未回款的订单,取订单金额为锁定信用。

(6)比较剩余信用和订单金额,确认是否能发起审批。

第四 编写代码

 

/**
     * 根据信用额度判断能否签订合同
     * @param $sales_contract
     * @return bool
     */
    public static function canContractByCredit($salesContractModel){
        $salesCustomersModel = $salesContractModel->salesCustomers;
        //判断是否先款后货
        if (\core\models\SalesContract::TYPE_FINANCE_BEFORE == $salesContractModel->sales_contract_settle_mode){
            return ['status'=>true, 'errcode'=>'0', 'errmsg'=>'ok'];
        }
        //判断上一单是否已经回款
        $lastestSaleOrderModels = \core\models\SalesOrder::find()->where([
            'is_del'=>0,
            'sales_customers_id'=>$salesCustomersModel->id,
            'sales_order_settle_mode'=>\core\models\SalesOrder::TYPE_WMS_BEFORE])->all();
        if (!empty($lastestSaleOrderModels)){
            foreach ($lastestSaleOrderModels as $lastestSaleOrderModel){
                $theSalesContractModel = $lastestSaleOrderModel->salesContract;
                $status = \core\models\CommonApproveStatus::getApprovalStatus($lastestSaleOrderModel->sales_order_code, \core\models\CommonDingtalkApproval::SALES_ORDER);
                if (empty($lastestSaleOrderModel->send_out_at)){
                    $send_out_at = 0;
                }else{
                    $send_out_at = $lastestSaleOrderModel->send_out_at;
                }
                if($status == \core\models\DingtalkApproval::COMPLETE_APPROVAL
                    && (time() - $send_out_at > $theSalesContractModel->sales_contract_aging * 3600 * 24)
                    && $lastestSaleOrderModel->send_out_status
                    && empty($lastestSaleOrderModel->repay_status)){
                    return ['status'=>false, 'errcode'=>'050204', 'errmsg'=>'抱歉,'.$lastestSaleOrderModel->sales_order_code.'上一单超账期未回款'];
                }
            }
        }

        //判断信用额度是否已经过期
        $salesCustomersModel = \core\models\SalesCustomers::findOne(['id'=>$salesContractModel->sales_customers_id]);
        if (empty($salesCustomersModel->sales_customers_credit_sum)){
            return ['status'=>false, 'errcode'=>'050200', 'errmsg'=>'抱歉,客户未授信'];
        }
        if ($salesCustomersModel->sales_customers_credit_end_at < time()){
            return ['status'=>false, 'errcode'=>'050201', 'errmsg'=>'抱歉,客户资质已经过期'];
        }
        //判断信用额度是否已经不足
        if ($salesCustomersModel->sales_customers_credit_surplus < \common\models\Base::moneyBcmul($salesContractModel->sales_contract_receivables)){
            return ['status'=>false, 'errcode'=>'050202', 'errmsg'=>'抱歉,客户信用余额不足'];
        }
        //判断超客户最大回款账期
        if (intval($salesCustomersModel->sales_customers_credit_credit_period) < intval($salesContractModel->sales_contract_aging)){
            return ['status'=>false, 'errcode'=>'050203', 'errmsg'=>'抱歉,合同超客户最大回款账期'];
        }
        //根据未锁定信用额度判断能否发起合同审批
        $salesCustomersCreditInfo = self::getSalesCustomersCreditInfo($salesCustomersModel);
        $salesCustomersTotalCredit = $salesCustomersCreditInfo['data']['salesCustomersTotalCredit'];
        $salesCustomersLockedCredit = $salesCustomersCreditInfo['data']['salesCustomersLockedCredit'];
        $salesCustomersUnlockedCredit = $salesCustomersCreditInfo['data']['salesCustomersUnlockedCredit'];
        if ($salesCustomersCreditInfo['data']['salesCustomersUnlockedCredit'] < self::moneyBcmul($salesContractModel->sales_contract_amount)){
            return ['status'=>false, 'errcode'=>'050204', 'errmsg'=>'抱歉,此客户未锁定信用小于合同总金额;此客户的总信用额度为'.\common\models\Base::moneyBcdiv($salesCustomersTotalCredit).';已锁定信用额度为'.\common\models\Base::moneyBcdiv($salesCustomersLockedCredit).';未锁定信用额度为'.\common\models\Base::moneyBcdiv($salesCustomersUnlockedCredit)];
        }
        return ['status'=>true, 'errcode'=>'0', 'errmsg'=>'ok'];
    }

    /**
     * 根据信用额度判断能否下订单
     * @param $sales_contract
     * @return bool
     */
    public static function canOrderByCredit($salesOrderModel){
        $salesCustomersModel = $salesOrderModel->salesCustomers;
        $salesContractModel = $salesOrderModel->salesContract;

        //累计超过合同的10%,无法发起审批
        $sum_price = self::getSubmittedOrderSumPriceByContractCode($salesContractModel->sales_contract_code);
        $total_price = $sum_price + \common\models\Base::moneyBcdiv($salesOrderModel->sales_order_total_amount);
        if ($total_price >= $salesContractModel->sales_contract_amount * 1.1){
            return ['status'=>false, 'errcode'=>'050299', 'errmsg'=>'抱歉,此合同的累计订单金额超过合同的110%,无法发起审批'];
        }

        //判断是否先款后货
        if (\core\models\SalesOrder::TYPE_FINANCE_BEFORE == $salesOrderModel->sales_order_settle_mode){
            return ['status'=>true, 'errcode'=>'0', 'errmsg'=>'ok'];
        }
        //判断上一单是否已经回款
        $lastestSaleOrderModels = \core\models\SalesOrder::find()->where([
            'is_del'=>0,
            'sales_customers_id'=>$salesCustomersModel->id,
            'sales_order_settle_mode'=>\core\models\SalesOrder::TYPE_WMS_BEFORE])->andWhere(['!=', 'id', $salesOrderModel->id])->all();
        if (!empty($lastestSaleOrderModels)){
            foreach ($lastestSaleOrderModels as $lastestSaleOrderModel){
                $theSalesContractModel = $lastestSaleOrderModel->salesContract;
                $status = \core\models\CommonApproveStatus::getApprovalStatus($lastestSaleOrderModel->sales_order_code, \core\models\CommonDingtalkApproval::SALES_ORDER);
                if (empty($lastestSaleOrderModel->send_out_at)){
                    $send_out_at = 0;
                }else{
                    $send_out_at = $lastestSaleOrderModel->send_out_at;
                }
                if($status == \core\models\DingtalkApproval::COMPLETE_APPROVAL
                && (time() - $send_out_at > $theSalesContractModel->sales_contract_aging * 3600 * 24)
                && $lastestSaleOrderModel->send_out_status
                && empty($lastestSaleOrderModel->repay_status)){
                    return ['status'=>false, 'errcode'=>'050204', 'errmsg'=>'抱歉,'.$lastestSaleOrderModel->sales_order_code.'上一单超账期未回款'];
                }
            }
        }

        //判断信用额度是否已经过期
        $salesCustomersModel = \core\models\SalesCustomers::findOne(['id'=>$salesOrderModel->sales_customers_id]);
        if (empty($salesCustomersModel->sales_customers_credit_sum)){
            return ['status'=>false, 'errcode'=>'050200', 'errmsg'=>'抱歉,客户未授信'];
        }
        if ($salesCustomersModel->sales_customers_credit_end_at < time()){
            return ['status'=>false, 'errcode'=>'050206', 'errmsg'=>'抱歉,客户资质已经过期'];
        }
        //判断信用额度是否已经不足
        if ($salesCustomersModel->sales_customers_credit_surplus < $salesOrderModel->sales_order_total_amount){
            return ['status'=>false, 'errcode'=>'050207', 'errmsg'=>'抱歉,客户信用余额不足'];
        }
        //判断订单额度是否超出合同额度
        if ($salesContractModel->sales_contract_amount < \common\models\Base::moneyBcdiv($salesOrderModel->sales_order_total_amount)){
            return ['status'=>false, 'errcode'=>'050208', 'errmsg'=>'抱歉,订单总金额超出合同总金额'];
        }
        //根据未锁定信用额度判断能否发起订单审批
        $salesCustomersCreditInfo = self::getSalesCustomersCreditInfo($salesCustomersModel);
        $salesCustomersTotalCredit = $salesCustomersCreditInfo['data']['salesCustomersTotalCredit'];
        $salesCustomersLockedCredit = $salesCustomersCreditInfo['data']['salesCustomersLockedCredit'];
        $salesCustomersUnlockedCredit = $salesCustomersCreditInfo['data']['salesCustomersUnlockedCredit'];
        if ($salesCustomersCreditInfo['data']['salesCustomersUnlockedCredit'] < $salesOrderModel->sales_order_total_amount){
            return ['status'=>false, 'errcode'=>'050204', 'errmsg'=>'抱歉,此客户未锁定信用小于订单总金额;此客户的总信用额度为'.\common\models\Base::moneyBcdiv($salesCustomersTotalCredit).';已锁定信用额度为'.\common\models\Base::moneyBcdiv($salesCustomersLockedCredit).';未锁定信用额度为'.\common\models\Base::moneyBcdiv($salesCustomersUnlockedCredit)];
        }
        return ['status'=>true, 'errcode'=>'0', 'errmsg'=>'ok'];
    }

    /**
     * @param $salesCustomersModel
     * 获取客户信用额度锁定信息
     */
    public static function getSalesCustomersCreditInfo($salesCustomersModel){
        $salesCustomersTotalCredit = $salesCustomersModel->sales_customers_credit_sum;
        $salesOrderModels = $salesCustomersModel->salesOrder;
        $salesCustomersLockedCredit = 0;
        $salesCustomersUnlockedCredit = $salesCustomersTotalCredit;
        foreach ($salesOrderModels as $salesOrderModel){
            if (empty($salesOrderModel->is_del)
                && empty($salesOrderModel->sales_order_settle_mode)){
                $status = \core\models\CommonApproveStatus::getApprovalStatus($salesOrderModel->sales_order_code, \core\models\CommonDingtalkApproval::SALES_ORDER);
                if (\core\models\DingtalkApproval::THE_APPROVAL == $status){
                    $salesCustomersLockedCredit += $salesOrderModel->sales_order_total_amount;
                }elseif (\core\models\DingtalkApproval::IN_THE_APPROVAL == $status){
                    $salesCustomersLockedCredit += $salesOrderModel->sales_order_total_amount;
                }elseif(\core\models\DingtalkApproval::COMPLETE_APPROVAL == $status
                    && empty($salesOrderModel->repay_status)){
                    $salesCustomersLockedCredit += $salesOrderModel->sales_order_total_amount;
                }else {

                }
            }
        }
        $salesCustomersUnlockedCredit = $salesCustomersTotalCredit - $salesCustomersLockedCredit;
        return $salesCustomersCreditInfo = ['status'=>true, 'errcode'=>'0', 'errmsg'=>'OK', 'data'=>[
            'salesCustomersTotalCredit'=>$salesCustomersTotalCredit,
            'salesCustomersLockedCredit'=>$salesCustomersLockedCredit,
            'salesCustomersUnlockedCredit'=>$salesCustomersUnlockedCredit,
        ]];
    }