openbuildings/purchases

多店铺购买

安装: 154,427

依赖项: 2

建议者: 0

安全: 0

星标: 2

关注者: 11

分支: 0

开放问题: 2

类型:kohana-module

0.12.1 2020-03-02 15:42 UTC

README

Build Status Scrutinizer Quality Score Code Coverage Latest Stable Version

这是一个Kohana模块,它为多品牌购买提供开箱即用的功能(每个购买可能包含来自不同卖家的物品,每个卖家独立处理其产品部分)

目前支持eMerchantPay和Paypal

安装

所有购买模型都开箱即用。但是为了正确使用,您需要通过实现Sellable接口来配置您想要出售的模型。例如

class Model_Product extends Jam_Model implements Sellable {

	public static function initialize(Jam_Meta $meta)
	{
		$meta
			->fields(array(
				'id' => Jam::field('primary'),
				'name' => Jam::field('string'),
				'currency' => Jam::field('string'),
				'price' => Jam::field('price'),
			));
	}

	public function price_for_purchase_item(Model_Purchase_Item $item)
	{
		return $this->price;
	}

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

您需要为您用户模型添加“Buyer”行为。它添加了current_purchasepurchases关联

class Model_User extends Kohana_Model_User {

	public static function initialize(Jam_Meta $meta)
	{
		$meta
			->behaviors(array(
				'buyer' => Jam::association('buyer'),
			));
		// ...
	}
}

购买、品牌购买和购买项目

购买的基本结构是一个购买,表示用户的视图,其中包含多个Brand_Purchase对象,每个品牌一个,并且每个对象都有一个“Purchase_Items”与之关联。

价格

此模块大量使用jam-monetary - 其所有与价格相关的方方法和字段都是Jam_Price对象,允许您安全地进行价格计算,从而使货币转换透明进行。更多信息请参阅:https://github.com/OpenBuildings/jam-monetary

购买项目标志

购买项目具有重要的“标志”

  • is_payable - 这意味着这是一个应该由买家“支付”的项目,并将添加到其总账单中。这对于某些项目仅需要对卖家可见(并计算),但不应出现在买家的账单上。
  • is_discount - 这意味着购买项目的价格应该是负值。这强制进行验证 - 折扣项目只能有负价格,而正常项目必须始终有正价格。

项目查询 您可以使用items()方法查询品牌购买的项目

$brand_purchase->items(); // return all the purchase items as an array
$brand_purchase->items('product'); // return all the purchase items with model "purchase_item_product" as an array
$brand_purchase->items(array('product', 'shipping')); // return all the purchase items with model "purchase_item_product" or "purchase_item_shipping" as an array
$brand_purchase->items(array('is_payable' => TRUE)); // return all the purchase items with flag "is_payable" set to TRUE as an array
$brand_purchase->items(array('is_payable' => TRUE, 'product')); // return all the purchase items with flag "is_payable" set to TRUE and are with model "purchase_item_product" as an array
$brand_purchase->items(array('not' => 'shipping')); // return all the purchase items that are not instance of model "purchase_item_shipping"

所有这些类型的查询都可以用于items_count()total_price()

还有一个“items_quantity”,它总结了所有匹配过滤器的项目的数量。

$brand_purchase->items_count(array('product', 'shipping'));
$brand_purchase->total_price(array('is_payable' = TRUE));
$brand_purchase->items_quantity(array('is_payable' = TRUE));

所有这些方法也可以在Model_Purchase对象上执行,为您提供所有品牌购买的汇总。例如

// This will return the quantity of all the payable items in all the brand_purchases of this purchase.
$purchase->items_quantity(array('is_payable' => TRUE));

在Model_Brand_Purchase对象上有一个特殊的方法,仅在Model_Brand_Purchase对象上可用。 total_price_ratio - 它将返回整个购买中特定品牌购买的部分(从0到1)。您也可以向它传递过滤器,以便仅考虑某些购买项目。

$brand_purchase->total_price_ratio(array('is_payable' => TRUE)); // Will return e.g. 0.6

价格冻结

通常,所有购买项目的价格都是动态生成的,通过在引用对象(无论是产品、促销等)上调用->price_for_purchase_item()方法,并使用当前的货币汇率计算得出。一旦您在购买上调用freeze()方法(并保存它),汇率和价格都设置为购买,不允许进一步修改购买,即使引用的价格发生变化,购买项目的价格也将保持冻结时的状态。

$purchase
	->freeze()
	->save();

如果您想修改购买,您必须调用unfreeze()它。另外,如果您想了解购买的状态,有一个isFrozen标志。

$purchase->unfreeze();
$purchase->isFrozen();

一旦购买被冻结并保存,对冻结字段/关联的任何更改都将被视为验证错误。

可冻结特性

为了使功能能够在所有购买模型上工作,使用了clippings/freezable包。它具有一些有用的特性,可以冻结一些值或集合。

class Model_Purchase extends Jam_Model {

	use Clippings\Freezable\FreezableCollectionTrait {
		performFreeze as freezeCollection;
		performUnfreeze as unfreezeCollection;
	};

	public static function initialize(Jam_Meta $meta)
	{
		$meta
			->associations(array(
				'brand_purchases' => Jam::association('has_many'),
			))
			->fields(array(
				'is_frozen' => Jam::field('boolean'),
				'price' => Jam::field('serializable'),
			));
	}

	public function price()
	{
		return $this->isFrozen() ? $this->price : $this->computePrice();
	}

	public function isFrozen()
	{
		return $this->is_frozen;
	}

	public function setFrozen($frozen)
	{
		$this->is_frozen = (bool) $frozen;

		return $this;
	}

	public function performFreeze()
	{
		$this->freezeCollection();

		$this->price = $this->price();
	}

	public function performUnfreeze()
	{
		$this->unfreezeCollection();

		$this->price = NULL;
	}

	public function getItems()
	{
		return $this->books;
	}
	//...
}

这意味着每次模型被“冻结”时,名为“price”的字段将分配给“price()”方法的结果。所有关联也将被“冻结”。为了使此功能正常工作,关联本身必须是可冻结的(实现FreezableInterface接口)。而且,price()方法和任何其他字段都必须考虑对象是否被冻结。例如:

public function price()
{
	return $this->isFrozen() ? $this->price : $this->compute_price();
}

添加/更新单个项目

您可以使用add_item()方法向购买中添加一个项目。它将在所有brand_items中搜索购买项目,如果在其他地方找到了相同的项,则更新其数量;否则,将其添加到适当的brand_item中(如果不存在,则创建该item)。


$purchse
	->add_item($brand, $new_purchase_item);

EMP处理器

要使用emp处理器,您需要在您的页面上有一个表单(您可以使用包含的Model_Emp_Form)。为处理器提供cc数据。

在控制器中

class Controller_Payment extends Controller_Template {

	public function action_index()
	{
		$purchase = // Load purchase from somewhere

		$form = Jam::build('emp_form', array($this->post()));
		if ($this->request->method() === Request::POST AND $form->check())
		{
			$purchase
				->build('payment', array('model' => 'payment_emp'))
					->execute($form->as_array());

			$this->redirect('payment/complete');
		}

		$this->template->content = View::factory('payment/index', array('form' => Jam::form($form)))
	}
}

表单是您视图中的一个简单的Jam_Form。

<form action='payment/index'>
	<?php echo $form->row('input', 'card_holder_name') ?>
	<?php echo $form->row('input', 'card_number') ?>
	<?php echo $form->row('input', 'exp_month') ?>
	<?php echo $form->row('input', 'exp_year') ?>
	<?php echo $form->row('input', 'cvv') ?>
	<button type="submit">Process payment</button>
</form>

EMP VBV处理器

这使用EMP信用卡处理器,但利用VBV/3DSecure的授权和执行方法。

在控制器中

class Controller_Payment extends Controller_Template {

	public function action_index()
	{
		$purchase = // Load purchase from somewhere

		$form = Jam::build('emp_form', array($this->post()));
		if ($this->request->method() === Request::POST AND $form->check())
		{
			$purchase
				->build('payment', array('model' => 'payment_paypal_vbv'))
					->authorize($form->vbv_params('/payment/complete'));

			// We need to save the form somewhere as it is later used for execute method
			$this->session->set('emp_form', $form->as_array());

			$this->redirect($purchase->payment->authorize_url());
		}

		$this->template->content = View::factory('payment/index', array('form' => Jam::form($form)));
	}

	public function action_complete()
	{
		$purchase = // Load purchase from somewhere

		if ( ! $purchase->is_paid())
		{
			$form = Jam::build('emp_form', array($this->session->get_once('emp_form')));

			$purchase
				->payment
					->execute($form->as_array());
		}

		$this->template->content = View::factory('payment/complete', array('purchase' => $purchase));
	}
}

表单是您视图中的一个简单的Jam_Form。

<form action='payment/index'>
	<?php echo $form->row('input', 'card_holder_name') ?>
	<?php echo $form->row('input', 'card_number') ?>
	<?php echo $form->row('input', 'exp_month') ?>
	<?php echo $form->row('input', 'exp_year') ?>
	<?php echo $form->row('input', 'cvv') ?>
	<button type="submit">Process payment</button>
</form>

PayPal处理器

PayPal交易需要3个步骤——创建交易、通过PayPal界面由用户授权,以及执行已授权的交易。

使用$processor->next_url()转到PayPal授权页面。

class Controller_Payment extends Controller_Template {

	public function action_index()
	{
		$purchase = // Load purchase from somewhere

		if ($this->request->method() === Request::POST AND $form->check())
		{
			$purchase
				->build('payment', array('model' => 'payment_paypal'))
					->authorize(array('success_url' => '/payment/complete', 'cancel_url' => '/payment/canceled'));

			$this->redirect($purchase->payment->authorize_url());
		}

		$this->template->content = View::factory('payment/index');
	}

	public function action_complete()
	{
		$purchase = // Load purchase from somewhere

		$purchase
			->payment
				->execute(array('payer_id' => Request::initial()->query('PayerID')));

		$this->template->content = View::factory('payment/complete', array('purchase' => $purchase));
	}
}

附加账单信息

您可以通过使用账单关联,将附加信息(如账单地址/名称)传递给支付处理器。

$purchase = Jam::find('purchase', 1);

$purchase->billing_address = Jam::build('address', array(
	'email' => 'john.smith@example.com',
	'first_name' => 'John',
	'last_name' => 'Smith',
	'line1' => 'Street 1',
	'city' => Jam::find('location', 'London'),
	'country' => Jam::find('location', 'United Kingdom'),
	'zip' => 'QSZND',
	'phone' => '1234567',
));

退款

退款通过特殊的Model_Brand_Refund对象执行——每个退款都特定于一个品牌购买——如果您未设置任何自定义项目,则所有项目都将被退款(整个交易);否则,您可以添加Model_Brand_Refund_Item对象以退款特定项目(部分退款)。

$brand_purchase = // Load brand purchase

$refund = $brand_purchase->refunds->create(array(
	'items' => array(
		// The whole price of a specific item
		array('purchase_item' => $brand_purchase->items[0])

		// Parital amount of an item
		array('purchase_item' => $brand_purchase->items[1], 'amount' => 100)
	)
));

$refund
	->execute();

稍后,您可以从品牌购买中检索退款或发出多次退款。

扩展支付

支付模型上的execute()authorize()refund()方法分别触发某些事件并保存模型。

  • model.before_execute
  • model.after_execute
  • model.before_authorize
  • model.after_authorize
  • model.before_refund
  • model.after_refund

前置方法在执行任何支付操作之前执行。后置方法在支付操作完成后(金钱已发送)并且模型被保存之后执行。以下是您可以使用它的方法:

public function initialize(Jam_Meta $meta, $name)
{
	parent::initialize($meta, $name);

	$meta
		->events()
			->bind('model.before_execute', array($this, 'change_status'))
			->bind('model.before_execute', array($this, 'add_fees'))
			->bind('model.after_execute', array($this, 'send_user_emails'));
}

public static function change_status(Model_Payment $payment, Jam_Event_Data $data)
{
	foreach ($payment->get_insist('purchase')->brand_purchases as $brand_purchase)
	{
		$brand_purchase->status = Model_Brand_Purchase::PAID;
	}

	$payment->purchase = $payment->purchase;
}
//...

退款事件还将Model_Brand_Refund对象作为第三个参数发送。

更新项目和扩展更新

两者brand_purchasepurchase都有一个update_items方法,它触发brand_purchase的事件'model.update_items'。这主要用于外部模块,这些模块可以挂钩到购买,并在触发该事件时添加/更新purchase_items。例如,openbuildings/shipping模块使用它来添加/更新运输项目。

例如,我们可能会有这样的行为:

class Jam_Behavior_MyBehavios extends Jam_Behavior {

	public function initialize(Jam_Meta $meta, $name)
	{
		parent::initialize($meta, $name);

		$meta
			->events()
				->bind('model.update_items', array($this, 'update_items'));
	}

	public function update_items(Model_Brand_Purchase $brand_purchase, Jam_Event_Data $data)
	{
		if ( ! $brand_purchase->items('shipping'))
		{
			$brand_purchase->items []= Jam::build('purchase_item_shipping', array(
				'reference' => $brand_purchase->shipping // some shipping object
			));
		}
	}
}

扩展过滤器

items()items_count()total_price()使用过滤器数组作为参数。它有一个特殊的事件model.filter_items,您可以在行为中使用它来添加额外的过滤器或扩展现有的过滤器。

以下是如何做到这一点的示例

class Jam_Behavior_MyBehavios extends Jam_Behavior {

	public function initialize(Jam_Meta $meta, $name)
	{
		parent::initialize($meta, $name);

		$meta
			->events()
				->bind('model.filter_items', array($this, 'filter_items'));
	}

	public function filter_shipping_items(Model_Brand_Purchase $brand_purchase, Jam_Event_Data $data, array $items, array $filter)
	{
		$items = is_array($data->return) ? $data->return : $items;
		$filtered = array();

		foreach ($items as $item)
		{
			if (array_key_exists('shippable', $filter) AND ($item->reference instanceof Shippable) !== $filter['shippable'])
			{
				continue;
			}

			$filtered []= $item;
		}

		$data->return = $filtered;
	}
}

运行测试

应使用运行在4444本地端口上的selenium运行测试。

例如:

xvfb-run java -jar vendor/claylo/selenium-server-standalone/selenium-server-standalone-2.*.jar

许可证

版权(c)2012-2013,OpenBuildings Ltd。由Ivan Kerin作为clippings.com的一部分开发。

根据BSD-3-Clause许可证,请参阅LICENSE文件。