跳转到主要内容
TimYao 提交于 1 May 2015

需求

客户需求是这样的: 网站的订单支付不要求全额支付(因为是B2B的业务),只需要支付一个定金就可以。 根据不同的user类型(A,B)以及他们是否在试用期,来要求支付不同的比例。如果A,定金为订单总额的50%。如果B,并且在试用期,30%。B不在试用期,则免定金,即跳过支付这个环节。

现状

Drupal Commerce并没有原生的定金功能。展开地毯式搜索,得到几个相关模块如下,但没有一个是成熟模块。

  • commerce_deposit 业务逻辑和我的需求有些差异,放弃。但有兴趣的朋友可以看一下代码,还是比较简单的。
  • Commerce Partial Payment  这个思路不错,也是最后我采用的办法。

与几位熟悉Drupal Commerce的朋友交流,现有的思路一般是通过添加price component或者custom line item来实现。我也尝试了第二种方法,具体的代码可以参考Commerce discount的模块。问题不大,如果作为折扣,这个line item作为负值没有什么问题,但作为定金,一个负值就看起来比较奇怪。容易让用户很困惑,并且这个方法最后改变了订单的总额。而从需求出发,订单总额是不应该改变的。所以这种方式最后也放弃了。

解决方案

参考Commerce Partial Payment ,基本实现了我需要的功能。我假设新建一个模块叫my_deposit。

定金计算

基本思路是这样的,首先写一个函数判断该用户是否需要定金,代码如下:

function my_deposit_is_desposit_required($uid){

  //your code here, return boolean

  return $required;
}

另外再写一个函数用来计算该用户该订单的定金数额,代码如下:

function my_deposit_get_deposit_amount($uid,$order_total){

  //your code here, get the deposit rate for this user

  $deposit_amount = $order_total*$rate;
  return $deposit_amount;
}

修改checkout页面

然后在checkout的付款页面,我们用 hook_form_BASE_FORM_ID_alter() 来修改commerce_checkout_form。基本思路如下: 首先调用上面的函数,看该用户是否需要付定金,如果不需要,则把payment methods 都删掉,这样就直接可以跳过payment步骤了。同时,在checkout form里加一个message给用户,说明定金数量或者你不需要支付定金。具体细节可看代码注释。

/**
 * Implements hook_form_BASE_FORM_ID_alter().
 *
 * Alters whichever commerce checkout page includes the commerece_payment
 * checkout pane and adds the amount element to the payment details.
 */
function my_deposit_form_commerce_checkout_form_alter(&$form, &$form_state, $form_id) {

  // If this is not the checkout page that the payment pane is on then we have nothing to do here.
  $panes = commerce_checkout_panes(array('pane_id' => 'commerce_payment'));
  $checkout_page = $panes['commerce_payment']['page'];
  if ($form_id != "commerce_checkout_form_$checkout_page" ) {
    return;
  }

  //check user, see if deposit payment is required.
  //if not required, pass the payment requirement.
  global $user;
  if(!my_deposit_is_desposit_required($user->uid)){
    //remove payment methods
    unset($form['commerce_payment']);
    $deposit_notice = "<div id='my-checkout-deposit-note'>You don't need to pay deposit. Please continue to complete your order.</div>";
    $form['checkout_review']['mh_deposit'] = array(
      '#markup' => $deposit_notice,
    );
  }else{

    //Add the deposit amount notice to the payment details form.
    $order_amount = commerce_payment_order_balance($form_state['order']);
    $deposit = my_deposit_get_deposit_amount($user->uid,$order_amount['amount']);
    $deposit = commerce_currency_amount_to_decimal($deposit, 'AUD');
    $deposit_notice = "<div id='my-checkout-deposit-note'>You need to pay deposit amount: $".$deposit."</div>";

    $form['commerce_payment']['my_deposit'] = array(
      '#markup' => $deposit_notice,
      '#weight' => -1,
    );
  }

}

修改payment method表单的submit callback

/**
 * Implements hook_commerce_checkout_pane_info_alter().
 *
 * Override callbacks for some checkout panes that pertain to order payment and
 * completion.
 */
function mh_deposit_commerce_checkout_pane_info_alter(&$checkout_panes) {

  // Override submit handlers for the commerce_payment pane so
  // that we can pass it into the payment method callback.

  $checkout_panes['commerce_payment']['callbacks']['checkout_form_submit'] = 'my_deposit_checkout_submit';

  // Over the form callback for the checkout_completion_message pane so that we
  // can show a different message if they made a partial payment that did not
  // complete payment for the order.
  //$checkout_panes['checkout_completion_message']['callbacks']['checkout_form'] = 'commerce_partial_payment_message_form';
}

下面是我们自己的callback,大部分是从commerce payment模块里的commerce_payment_pane_checkout_form_submit()里copy来的,47-56行是我们自己的修改。因为没有hook,所以目前只能这么改了。 其实很简单,就是把提交给payment method的金额修改为我们自己的定金金额。

/**
 * Submit callback function for the commerce_payment checkout pane.
 *
 * Calls the appropriate callback function for the selected payment method with
 * the amount specified by the user.
 *
 * @see commerce_payment_pane_checkout_form_submit()
 *
 * @todo This function reproduces a lot of code from
 * commerce_payment_pane_checkout_form_submit(). Patch commerce_payment to add a
 * hook implementation to accomplish what we need instead.
 */
function my_deposit_checkout_submit($form, &$form_state, $checkout_pane, $order) {

  global $user;
  // Check to make sure there are no validation issues with other form elements
  // before executing payment method callbacks.
  if (form_get_errors()) {
    drupal_set_message(t('Your payment will not be processed until all errors on the page have been addressed.'), 'warning');
    return FALSE;
  }

  // Only submit if we actually had payment methods on the form.
  $pane_id = $checkout_pane['pane_id'];
  if (!empty($form[$pane_id]) && !empty($form_state['values'][$pane_id])) {
    $pane_form = $form[$pane_id];
    $pane_values = $form_state['values'][$pane_id];

    // Only process if there were payment methods available.
    if ($pane_values['payment_methods']) {
      // If an amount was specified then we need to use that rather than the
      // order balance.

      // If we can calculate a single order total for the order...
      $order->data['payment_method'] = $pane_values['payment_method'];
      if ($amount = commerce_payment_order_balance($order)) {
        // Delegate submit to the payment method callback.
        $payment_method = commerce_payment_method_instance_load($pane_values['payment_method']);
        $callback = commerce_payment_method_callback($payment_method, 'submit_form_submit');
        if ($callback) {
          // Initialize the payment details array to accommodate payment methods
          // that don't add any additional details to the checkout pane form.
          if (empty($pane_values['payment_details'])) {
            $pane_values['payment_details'] = array();
          }

          $order_total = $order->commerce_order_total['und'][0]['amount'];
          $amount['amount'] = mh_deposit_get_deposit_amount($user->uid,$order_total);

          // If payment fails, rebuild the checkout form without progressing.
          $result = $callback($payment_method, $pane_form['payment_details'], $pane_values['payment_details'], $order, $amount);
          if ($result === FALSE) {
            $form_state['rebuild'] = TRUE;
          }
          else {
            //rules_invoke_all('commerce_partial_paryment_received', $order, $amount);
          }
        }
      }
    }
  }
  else {
    // If there were no payment methods on the form, check to see if the pane is
    // configured to trigger "When an order is first paid in full" on submission
    // for free orders.
    $behavior = variable_get('commerce_payment_pane_no_method_behavior', COMMERCE_PAYMENT_PANE_NO_METHOD_MESSAGE);

    if (in_array($behavior, array(COMMERCE_PAYMENT_PANE_NO_METHOD_EMPTY_EVENT, COMMERCE_PAYMENT_PANE_NO_METHOD_MESSAGE_EVENT))) {
      // Check the balance of the order.
      $balance = commerce_payment_order_balance($order);

      if (!empty($balance) && $balance['amount'] <= 0) {
        // Trigger the event now for free orders, simulating payment being
        // submitted on pane submission that brings the balance to 0. Use an
        // empty transaction, as we wouldn't typically save a transaction where
        // a financial transaction has not actually occurred.
        rules_invoke_all('commerce_payment_order_paid_in_full', $order, commerce_payment_transaction_new('', $order->order_id));

        // Update the order's data array to indicate this just happened.
        $order->data['commerce_payment_order_paid_in_full_invoked'] = TRUE;
      }
    }
  }
}

这样就算基本完工了,测试一下你可以看到信用卡付款的金额已经变成了定金金额。订单完成后,查看order,可以看到付款记录,order balance是扣除定金余下的部分。 我们的需求并不涉及余款的支付,客户将会通过其它方式支付余款。所以这里就不讨论了。

注意事项

Paypal等offsite payment method的问题

以上方法在paypal上不起作用,我没有太深究原因,基本上的原因就是Paypal没有上面的callback提交,所以我们的蓝色代码不会被运行。暂时的解决办法就是直接用强大的hook_form_alter。直接修改commerce_paypal_wps_redirect_form。

/**
 * Because Paypal module skipped the above checkout form submit callback, so we have to treat it differently
 */
function mh_deposit_form_commerce_paypal_wps_redirect_form_alter(&$form, &$form_state, $form_id) {
  global $user;
  $order_total = $form['amount_1']['#value'];
  $deposit_amount = my_deposit_get_deposit_amount($user->uid,$order_total);
  $form['amount_1']['#value'] = $deposit_amount;
}

这样就搞定啦。 Paypal的Sandbox测试,IPN在localhost会有问题:

because in the paypal sandbox(for example) you must specify an IPN handler URL. Paypal is using this URL to call the php file on your website that handles the IPN call. So you cannot pass into that url localhost/yourwebsite/ipn_handler.php. You will need something like your_computer_public_ip/yourwebsite/ipn_handler.php

一些相关的有用的参考资料

原文链接 http://blogtimyao.goeggo.com/如何用drupal-commerce实现定金功能/

Drupal 版本