openbuildings / purchases
多店铺购买
Requires
- php: ^7.1
- clippings/freezable: ^0.3
- composer/installers: *
- kohana/core: ^3.3
- kohana/database: ^3.3.4
- league/omnipay: ^3.0.2
- openbuildings/jam: ^0.6
- openbuildings/jam-auth: ^0.5.1
- openbuildings/jam-locations: ^0.2.1
- openbuildings/jam-monetary: ^0.2
- php-http/httplug: ^2.0
Requires (Dev)
Suggests
- openbuildings/promotions: Extends purchases functionality with promotions and gift cards
- openbuildings/shipping: Extends purchases functionality with shipping and delivery
- 0.12.1
- 0.12.0
- 0.11.0
- 0.10.5
- 0.10.4
- 0.10.3
- 0.10.2
- 0.10.1
- 0.10.0
- 0.9.4
- 0.9.3
- 0.9.2
- 0.9.1
- 0.9.0
- dev-master / 0.8.x-dev
- 0.8.5
- 0.8.4
- 0.8.3
- 0.8.2
- 0.8.1
- 0.8.0
- 0.8.0-rc.1
- 0.7.3
- 0.7.2
- 0.7.1
- 0.7.0
- 0.7.0-rc.3
- 0.7.0-rc.2
- 0.7.0-rc.1
- 0.7.0-alpha.3
- 0.7.0-alpha.2
- 0.7.0-alpha.1
- 0.6.x-dev
- 0.6.3
- 0.6.2
- 0.6.2-rc.1
- 0.6.1
- 0.6.0
- 0.5.34
- 0.5.32
- 0.5.31
- 0.5.30
- 0.5.29
- 0.5.28
- 0.5.27
- 0.5.26
- 0.5.25
- 0.5.24
- 0.5.23
- 0.5.22
- 0.5.21
- 0.5.20
- 0.5.19
- 0.5.18
- 0.5.17
- 0.5.16
- 0.5.15
- 0.5.14
- 0.5.13
- 0.5.12
- 0.5.11
- 0.5.10
- 0.5.9
- 0.5.8
- 0.5.7
- 0.5.6
- 0.5.5
- 0.5.4
- 0.5.3
- 0.5.2
- 0.5.1
- 0.5.0
- 0.4.5
- 0.4.4
- 0.4.3
- 0.4.2
- 0.4.1
- 0.4.0
- 0.3.24
- 0.3.23
- 0.3.22
- 0.3.21
- 0.3.20
- 0.3.19
- 0.3.18
- 0.3.17
- 0.3.16
- 0.3.15
- 0.3.14
- 0.3.13
- 0.3.12
- 0.3.11
- 0.3.10
- 0.3.9
- 0.3.8
- 0.3.7
- 0.3.6
- 0.3.5
- 0.3.4
- 0.3.3
- 0.3.2
- 0.3.1
- 0.3.0
- 0.2.6
- 0.2.1
- 0.2.0
- 0.1.4
- 0.1.3
- 0.1.2
- 0.1.1
- 0.1.0
- dev-improve-static-analysis
This package is auto-updated.
Last update: 2024-08-29 03:31:23 UTC
README
这是一个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_purchase
和purchases
关联
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_purchase
和purchase
都有一个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文件。