1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* BoxBilling |
4
|
|
|
* |
5
|
|
|
* @copyright BoxBilling, Inc (http://www.boxbilling.com) |
6
|
|
|
* @license Apache-2.0 |
7
|
|
|
* |
8
|
|
|
* Copyright BoxBilling, Inc |
9
|
|
|
* This source file is subject to the Apache-2.0 License that is bundled |
10
|
|
|
* with this source code in the file LICENSE |
11
|
|
|
*/ |
12
|
|
|
|
13
|
|
|
namespace Box\Mod\Cart; |
14
|
|
|
|
15
|
|
|
use Box\InjectionAwareInterface; |
16
|
|
|
|
17
|
|
|
class Service implements InjectionAwareInterface |
18
|
|
|
{ |
19
|
|
|
|
20
|
|
|
protected $di; |
21
|
|
|
|
22
|
43 |
|
public function setDi($di) |
23
|
|
|
{ |
24
|
43 |
|
$this->di = $di; |
25
|
43 |
|
} |
26
|
|
|
|
27
|
1 |
|
public function getDi() |
28
|
|
|
{ |
29
|
1 |
|
return $this->di; |
30
|
|
|
} |
31
|
|
|
|
32
|
1 |
|
public function getSearchQuery($data) |
33
|
|
|
{ |
34
|
|
|
$sql = " |
35
|
|
|
SELECT cart.id FROM cart |
36
|
|
|
LEFT JOIN currency ON cart.currency_id = currency.id |
37
|
1 |
|
LEFT JOIN promo ON cart.promo_id = promo.id"; |
38
|
|
|
|
39
|
1 |
|
return array($sql, array()); |
40
|
|
|
} |
41
|
|
|
|
42
|
|
|
/** |
43
|
|
|
* @return \Model_Cart |
44
|
|
|
*/ |
45
|
8 |
|
public function getSessionCart() |
46
|
|
|
{ |
47
|
3 |
|
$sqlBindings = array(':session_id' => $this->di['session']->getId()); |
48
|
3 |
|
$cart = $this->di['db']->findOne('Cart', 'session_id = :session_id', $sqlBindings); |
49
|
|
|
|
50
|
3 |
|
if ($cart instanceof \Model_Cart) { |
51
|
1 |
|
return $cart; |
52
|
|
|
} |
53
|
|
|
|
54
|
2 |
|
$cc = $this->di['mod_service']('currency'); |
55
|
|
|
|
56
|
2 |
|
if ($this->di['session']->get('client_id')) { |
57
|
1 |
|
$client_id = $this->di['session']->get('client_id'); |
58
|
1 |
|
$currency = $cc->getCurrencyByClientId($client_id); |
59
|
1 |
|
} else { |
60
|
1 |
|
$currency = $cc->getDefault(); |
61
|
|
|
} |
62
|
|
|
|
63
|
8 |
|
$cart = $this->di['db']->dispense('Cart'); |
64
|
2 |
|
$cart->session_id = $this->di['session']->getId(); |
65
|
2 |
|
$cart->currency_id = $currency->id; |
66
|
2 |
|
$cart->created_at = date('Y-m-d H:i:s'); |
67
|
2 |
|
$cart->updated_at = date('Y-m-d H:i:s'); |
68
|
2 |
|
$this->di['db']->store($cart); |
69
|
|
|
|
70
|
2 |
|
return $cart; |
71
|
|
|
} |
72
|
|
|
|
73
|
6 |
|
public function addItem(\Model_Cart $cart, \Model_Product $product, array $data) |
74
|
|
|
{ |
75
|
6 |
|
$event_params = array_merge($data, array('cart_id' => $cart->id, 'product_id' => $product->id)); |
76
|
6 |
|
$this->di['events_manager']->fire(array('event' => 'onBeforeProductAddedToCart', 'params' => $event_params)); |
77
|
|
|
|
78
|
6 |
|
$productService = $product->getService(); |
79
|
|
|
|
80
|
6 |
|
if ($this->isRecurrentPricing($product)) { |
81
|
|
|
$required = array( |
82
|
2 |
|
'period' => 'Period parameter not passed', |
83
|
2 |
|
); |
84
|
2 |
|
$this->di['validator']->checkRequiredParamsForArray($required, $data); |
85
|
|
|
|
86
|
1 |
|
if (!$this->isPeriodEnabledForProduct($product, $data['period'])) { |
87
|
1 |
|
throw new \Box_Exception('Selected billing period is not valid'); |
88
|
|
|
} |
89
|
|
|
} |
90
|
|
|
|
91
|
4 |
|
$qty = $this->di['array_get']($data, 'quantity', 1); |
92
|
|
|
// check stock |
93
|
4 |
|
if (!$this->isStockAvailable($product, $qty)) { |
94
|
1 |
|
throw new \Box_Exception("I'm afraid we are out of stock."); |
95
|
|
|
} |
96
|
|
|
|
97
|
3 |
|
$addons = $this->di['array_get']($data, 'addons', array()); |
98
|
3 |
|
unset($data['id']); |
99
|
3 |
|
unset($data['addons']); |
100
|
|
|
|
101
|
3 |
|
$list = array(); |
102
|
3 |
|
$list[] = array( |
103
|
3 |
|
'product' => $product, |
104
|
3 |
|
'config' => $data, |
105
|
|
|
); |
106
|
|
|
|
107
|
|
|
//check for required domain product |
108
|
3 |
|
if (method_exists($productService, 'getDomainProductFromConfig')) { |
109
|
1 |
|
$dc = $productService->getDomainProductFromConfig($product, $data); |
110
|
1 |
|
if (isset($dc['config']) && $dc['product'] && $dc['product'] instanceof \Model_Product) { |
111
|
1 |
|
$list[] = $dc; |
112
|
1 |
|
} |
113
|
1 |
|
} |
114
|
|
|
|
115
|
3 |
|
$productService = $this->di['mod_service']('Product'); |
116
|
3 |
|
foreach ($addons as $id => $ac) { |
117
|
|
|
if (isset($ac['selected']) && (bool)$ac['selected']) { |
118
|
|
|
$addon = $productService->getAddonById($id); |
119
|
|
|
if ($addon instanceof \Model_Product) { |
120
|
|
|
if ($this->isRecurrentPricing($addon)) { |
121
|
|
|
|
122
|
|
|
$required = array( |
123
|
|
|
'period' => 'Addon period parameter not passed', |
124
|
|
|
); |
125
|
|
|
$this->di['validator']->checkRequiredParamsForArray($required, $ac); |
126
|
|
|
|
127
|
|
|
if (!$this->isPeriodEnabledForProduct($addon, $ac['period'])) { |
128
|
|
|
throw new \Box_Exception('Selected billing period is not valid for addon'); |
129
|
|
|
} |
130
|
|
|
} |
131
|
|
|
$ac['parent_id'] = $product->id; |
132
|
|
|
|
133
|
|
|
$list[] = array( |
134
|
|
|
'product' => $addon, |
135
|
|
|
'config' => $ac, |
136
|
|
|
); |
137
|
|
|
} else { |
138
|
|
|
error_log('Addon not found by id ' . $id); |
139
|
|
|
} |
140
|
|
|
} |
141
|
3 |
|
} |
142
|
|
|
|
143
|
3 |
|
foreach ($list as $c) { |
144
|
3 |
|
$productFromList = $c['product']; |
145
|
3 |
|
$productFromListConfig = $c['config']; |
146
|
|
|
|
147
|
3 |
|
$productServiceFromList = $productFromList->getService(); |
148
|
|
|
|
149
|
|
|
//@deprecated logic |
150
|
3 |
|
if (method_exists($productServiceFromList, 'prependOrderConfig')) { |
151
|
1 |
|
$productFromListConfig = $productServiceFromList->prependOrderConfig($productFromList, $productFromListConfig); |
152
|
1 |
|
} |
153
|
|
|
|
154
|
3 |
|
if (method_exists($productServiceFromList, 'attachOrderConfig')) { |
155
|
1 |
|
$model = $this->di['db']->load('Product', $productFromList->id); |
156
|
1 |
|
$productFromListConfig = $productServiceFromList->attachOrderConfig($model, $productFromListConfig); |
157
|
1 |
|
} |
158
|
3 |
|
if (method_exists($productServiceFromList, 'validateOrderData')) { |
159
|
2 |
|
$productServiceFromList->validateOrderData($productFromListConfig); |
160
|
2 |
|
} |
161
|
3 |
|
if (method_exists($productServiceFromList, 'validateCustomForm')) { |
162
|
1 |
|
$productServiceFromList->validateCustomForm($productFromListConfig, $this->di['db']->toArray($productFromList)); |
163
|
1 |
|
} |
164
|
3 |
|
$this->addProduct($cart, $productFromList, $productFromListConfig); |
165
|
3 |
|
} |
166
|
|
|
|
167
|
3 |
|
$this->di['logger']->info('Added "%s" to shopping cart', $product->title); |
168
|
|
|
|
169
|
3 |
|
$this->di['events_manager']->fire(array('event' => 'onAfterProductAddedToCart', 'params' => $event_params)); |
170
|
|
|
|
171
|
3 |
|
return true; |
172
|
|
|
} |
173
|
|
|
|
174
|
2 |
|
public function isStockAvailable(\Model_Product $product, $qty) |
175
|
|
|
{ |
176
|
2 |
|
if ($product->stock_control) { |
177
|
1 |
|
return ($product->quantity_in_stock >= $qty); |
178
|
|
|
} |
179
|
|
|
|
180
|
1 |
|
return TRUE; |
181
|
|
|
} |
182
|
|
|
|
183
|
1 |
|
public function isRecurrentPricing(\Model_Product $model) |
184
|
|
|
{ |
185
|
1 |
|
$productTable = $model->getTable(); |
186
|
1 |
|
$pricing = $productTable->getPricingArray($model); |
187
|
1 |
|
return (isset($pricing['type']) && $pricing['type'] == \Model_ProductPayment::RECURRENT); |
188
|
|
|
} |
189
|
|
|
|
190
|
2 |
|
public function isPeriodEnabledForProduct(\Model_Product $model, $period) |
191
|
|
|
{ |
192
|
2 |
|
$productTable = $model->getTable(); |
193
|
2 |
|
$pricing = $productTable->getPricingArray($model); |
194
|
2 |
|
if ($pricing['type'] == \Model_ProductPayment::RECURRENT) { |
195
|
1 |
|
return (bool)$pricing['recurrent'][$period]['enabled']; |
196
|
|
|
} |
197
|
|
|
|
198
|
1 |
|
return true; |
199
|
|
|
} |
200
|
|
|
|
201
|
1 |
|
protected function addProduct(\Model_Cart $cart, \Model_Product $product, array $data) |
202
|
|
|
{ |
203
|
1 |
|
$item = $this->di['db']->dispense('CartProduct'); |
204
|
1 |
|
$item->cart_id = $cart->id; |
205
|
1 |
|
$item->product_id = $product->id; |
206
|
1 |
|
$item->config = json_encode($data); |
207
|
1 |
|
$this->di['db']->store($item); |
208
|
|
|
|
209
|
1 |
|
return true; |
210
|
|
|
} |
211
|
|
|
|
212
|
2 |
|
public function removeProduct(\Model_Cart $cart, $id, $removeAddons = true) |
213
|
|
|
{ |
214
|
|
|
$bindings = array( |
215
|
2 |
|
':cart_id' => $cart->id, |
216
|
|
|
':id' => $id |
217
|
2 |
|
); |
218
|
|
|
|
219
|
2 |
|
$cartProduct = $this->di['db']->findOne('CartProduct', 'id = :id AND cart_id = :cart_id', $bindings); |
220
|
2 |
|
if (!$cartProduct instanceof \Model_CartProduct) { |
221
|
1 |
|
throw new \Box_Exception('Product not found'); |
222
|
|
|
} |
223
|
|
|
|
224
|
1 |
|
if ($removeAddons) { |
225
|
1 |
|
$allCartProducts = $this->di['db']->find('CartProduct', 'cart_id = :cart_id', array(':cart_id' => $cart->id)); |
226
|
1 |
|
foreach ((array)$allCartProducts as $cProduct) { |
227
|
1 |
|
$config = json_decode($cProduct->config, true); |
228
|
1 |
|
if (isset($config['parent_id']) && $config['parent_id'] == $cartProduct->product_id) { |
229
|
|
|
$this->di['db']->trash($cProduct); |
230
|
|
|
$this->di['logger']->info('Removed product addon from shopping cart'); |
231
|
|
|
} |
232
|
1 |
|
} |
233
|
1 |
|
} |
234
|
|
|
|
235
|
1 |
|
$this->di['db']->trash($cartProduct); |
236
|
|
|
|
237
|
1 |
|
$this->di['logger']->info('Removed product from shopping cart'); |
238
|
|
|
|
239
|
1 |
|
return true; |
240
|
|
|
} |
241
|
|
|
|
242
|
1 |
|
public function changeCartCurrency(\Model_Cart $cart, \Model_Currency $currency) |
243
|
|
|
{ |
244
|
1 |
|
$cart->currency_id = $currency->id; |
245
|
1 |
|
$this->di['db']->store($cart); |
246
|
|
|
|
247
|
1 |
|
$this->di['logger']->info('Changed shopping cart #%s currency to %s', $cart->id, $currency->title); |
248
|
|
|
|
249
|
1 |
|
return true; |
250
|
|
|
} |
251
|
|
|
|
252
|
1 |
|
public function resetCart(\Model_Cart $cart) |
253
|
|
|
{ |
254
|
1 |
|
$cartProducts = $this->di['db']->find('CartProduct', 'cart_id = :cart_id', array(':cart_id' => $cart->id)); |
255
|
1 |
|
foreach ($cartProducts as $cartProduct) { |
256
|
1 |
|
$this->di['db']->trash($cartProduct); |
257
|
1 |
|
} |
258
|
1 |
|
$cart->promo_id = NULL; |
259
|
1 |
|
$cart->updated_at = date('Y-m-d H:i:s'); |
260
|
1 |
|
$this->di['db']->store($cart); |
261
|
|
|
|
262
|
1 |
|
return true; |
263
|
|
|
} |
264
|
|
|
|
265
|
1 |
|
public function removePromo(\Model_Cart $cart) |
266
|
|
|
{ |
267
|
1 |
|
$cart->promo_id = NULL; |
268
|
1 |
|
$cart->updated_at = date('Y-m-d H:i:s'); |
269
|
1 |
|
$this->di['db']->store($cart); |
270
|
|
|
|
271
|
1 |
|
$this->di['logger']->info('Removed promo code from shopping cart #%s', $cart->id); |
272
|
|
|
|
273
|
1 |
|
return true; |
274
|
|
|
} |
275
|
|
|
|
276
|
3 |
|
public function applyPromo(\Model_Cart $cart, \Model_Promo $promo) |
277
|
|
|
{ |
278
|
3 |
|
if ($cart->promo_id == $promo->id) { |
279
|
1 |
|
return true; |
280
|
|
|
} |
281
|
|
|
|
282
|
2 |
|
if ($this->isEmptyCart($cart)) { |
283
|
1 |
|
throw new \Box_Exception('Add products to cart before applying promo code'); |
284
|
|
|
} |
285
|
|
|
|
286
|
1 |
|
$cart->promo_id = $promo->id; |
287
|
1 |
|
$this->di['db']->store($cart); |
288
|
|
|
|
289
|
1 |
|
$this->di['logger']->info('Applied promo code %s to shopping cart', $promo->code); |
290
|
|
|
|
291
|
1 |
|
return true; |
292
|
|
|
} |
293
|
|
|
|
294
|
1 |
|
protected function isEmptyCart(\Model_Cart $cart) |
295
|
|
|
{ |
296
|
1 |
|
$cartProducts = $this->di['db']->find('CartProduct', 'cart_id = :cart_id', array(':cart_id' => $cart->id)); |
297
|
|
|
|
298
|
1 |
|
return (count($cartProducts) == 0); |
299
|
|
|
} |
300
|
|
|
|
301
|
1 |
|
public function rm(\Model_Cart $cart) |
302
|
|
|
{ |
303
|
1 |
|
$cartProducts = $this->di['db']->find('CartProduct', 'cart_id = :cart_id', array(':cart_id' => $cart->id)); |
304
|
|
|
|
305
|
1 |
|
foreach ($cartProducts as $cartProduct) { |
306
|
1 |
|
$this->di['db']->trash($cartProduct); |
307
|
1 |
|
} |
308
|
|
|
|
309
|
1 |
|
$this->di['db']->trash($cart); |
310
|
|
|
|
311
|
1 |
|
return true; |
312
|
|
|
} |
313
|
|
|
|
314
|
1 |
|
public function toApiArray(\Model_Cart $model, $deep = false, $identity = null) |
315
|
|
|
{ |
316
|
1 |
|
$products = $this->getCartProducts($model); |
317
|
|
|
|
318
|
1 |
|
$currency = $this->di['db']->getExistingModelById('Currency', $model->currency_id); |
319
|
|
|
|
320
|
1 |
|
$items = array(); |
321
|
1 |
|
$total = 0; |
322
|
1 |
|
$cart_discount = 0; |
323
|
1 |
|
$items_discount = 0; |
324
|
1 |
|
foreach ($products as $product) { |
325
|
1 |
|
$p = $this->cartProductToApiArray($product); |
326
|
1 |
|
$total += $p['total'] + $p['setup_price']; |
327
|
1 |
|
$items_discount += $p['discount']; |
328
|
1 |
|
$items[] = $p; |
329
|
1 |
|
} |
330
|
|
|
|
331
|
1 |
|
if ($model->promo_id) { |
332
|
|
|
$promo = $this->di['db']->getExistingModelById('Promo', $model->promo_id, 'Promo not found'); |
333
|
|
|
$promocode = $promo->code; |
334
|
|
|
} else { |
335
|
1 |
|
$promocode = NULL; |
336
|
|
|
} |
337
|
|
|
|
338
|
1 |
|
$currencyService = $this->di['mod_service']('currency'); |
339
|
|
|
$result = array( |
340
|
1 |
|
'promocode' => $promocode, |
341
|
1 |
|
'discount' => $items_discount, |
342
|
1 |
|
'subtotal' => $total, |
343
|
1 |
|
'total' => $total - $items_discount, |
344
|
1 |
|
'items' => $items, |
345
|
1 |
|
'currency' => $currencyService->toApiArray($currency), |
346
|
|
|
); |
347
|
1 |
|
|
348
|
|
|
return $result; |
349
|
|
|
} |
350
|
4 |
|
|
351
|
|
|
public function isClientAbleToUsePromo(\Model_Client $client, \Model_Promo $promo) |
352
|
4 |
|
{ |
353
|
1 |
|
if (!$this->promoCanBeApplied($promo)) { |
354
|
|
|
return false; |
355
|
|
|
} |
356
|
3 |
|
|
357
|
1 |
|
if (!$promo->once_per_client) { |
358
|
|
|
return true; |
359
|
|
|
} |
360
|
2 |
|
|
361
|
|
|
return !$this->clientHadUsedPromo($client, $promo); |
362
|
|
|
} |
363
|
5 |
|
|
364
|
|
|
public function promoCanBeApplied(\Model_Promo $promo) |
365
|
5 |
|
{ |
366
|
1 |
|
if (!$promo->active) { |
367
|
|
|
return false; |
368
|
|
|
} |
369
|
4 |
|
|
370
|
1 |
|
if ($promo->maxuses && $promo->maxuses <= $promo->used) { |
371
|
|
|
return false; |
372
|
|
|
} |
373
|
3 |
|
|
374
|
1 |
|
if ($promo->start_at && (strtotime($promo->start_at) - time() > 0)) { |
375
|
|
|
return false; |
376
|
|
|
} |
377
|
2 |
|
|
378
|
1 |
|
if ($promo->end_at && (strtotime($promo->end_at) - time() < 0)) { |
379
|
|
|
return false; |
380
|
|
|
} |
381
|
1 |
|
|
382
|
|
|
return true; |
383
|
|
|
} |
384
|
6 |
|
|
385
|
|
|
public function isPromoAvailableForClientGroup(\Model_Promo $promo) |
386
|
6 |
|
{ |
387
|
|
|
$clientGroups = $this->di['tools']->decodeJ($promo->client_groups); |
388
|
6 |
|
|
389
|
2 |
|
if (empty($clientGroups)) { |
390
|
|
|
return true; |
391
|
|
|
} |
392
|
|
|
|
393
|
4 |
|
try { |
394
|
4 |
|
$client = $this->di['loggedin_client']; |
395
|
|
|
} catch (\Exception $e) { |
396
|
|
|
$client = null; |
397
|
|
|
} |
398
|
4 |
|
|
399
|
1 |
|
if (is_null($client)){ |
400
|
|
|
return false; |
401
|
|
|
} |
402
|
3 |
|
|
403
|
1 |
|
if (!$client->client_group_id) { |
404
|
|
|
return false; |
405
|
|
|
} |
406
|
2 |
|
|
407
|
|
|
return in_array($client->client_group_id, $clientGroups); |
408
|
|
|
} |
409
|
1 |
|
|
410
|
|
|
protected function clientHadUsedPromo(\Model_Client $client, \Model_Promo $promo) |
411
|
1 |
|
{ |
412
|
1 |
|
$sql = "SELECT id FROM client_order WHERE promo_id = :promo AND client_id = :cid LIMIT 1"; |
413
|
|
|
$promoId = $this->di['db']->getCell($sql, array(':promo' => $promo->id, ':cid' => $client->id)); |
414
|
1 |
|
|
415
|
|
|
return ($promoId !== null); |
416
|
|
|
} |
417
|
1 |
|
|
418
|
|
|
public function getCartProducts(\Model_Cart $model) |
419
|
1 |
|
{ |
420
|
|
|
return $this->di['db']->find('CartProduct', 'cart_id = :cart_id ORDER BY id ASC', array(':cart_id' => $model->id)); |
421
|
|
|
} |
422
|
2 |
|
|
423
|
|
|
public function checkoutCart(\Model_Cart $cart, \Model_Client $client, $gateway_id = null) |
424
|
2 |
|
{ |
425
|
2 |
|
if ($cart->promo_id) { |
426
|
2 |
|
$promo = $this->di['db']->getExistingModelById('Promo', $cart->promo_id, 'Promo not found'); |
427
|
1 |
|
if (!$this->isClientAbleToUsePromo($client, $promo)) { |
428
|
|
|
throw new \Box_Exception('You have already used this promo code. Please remove promo code and checkout again.', null, 9874); |
429
|
1 |
|
} |
430
|
|
|
} |
431
|
1 |
|
|
432
|
|
|
$this->di['events_manager']->fire( |
433
|
1 |
|
array( |
434
|
|
|
'event' => 'onBeforeClientCheckout', |
435
|
1 |
|
'params' => array( |
436
|
1 |
|
'ip' => $this->di['request']->getClientAddress(), |
437
|
1 |
|
'client_id' => $client->id, |
438
|
1 |
|
'cart_id' => $cart->id) |
439
|
1 |
|
) |
440
|
|
|
); |
441
|
1 |
|
|
442
|
|
|
list($order, $invoice, $orders) = $this->createFromCart($client, $gateway_id); |
443
|
1 |
|
|
444
|
|
|
$this->rm($cart); |
445
|
1 |
|
|
446
|
|
|
$this->di['logger']->info('Checked out shopping cart'); |
447
|
1 |
|
|
448
|
|
|
$this->di['events_manager']->fire( |
449
|
1 |
|
array( |
450
|
|
|
'event' => 'onAfterClientOrderCreate', |
451
|
1 |
|
'params' => array( |
452
|
1 |
|
'ip' => $this->di['request']->getClientAddress(), |
453
|
1 |
|
'client_id' => $client->id, |
454
|
1 |
|
'id' => $order->id |
455
|
1 |
|
) |
456
|
1 |
|
) |
457
|
|
|
); |
458
|
|
|
|
459
|
1 |
|
$result = array( |
460
|
1 |
|
'gateway_id' => $gateway_id, |
461
|
1 |
|
'invoice_hash' => null, |
462
|
1 |
|
'order_id' => $order->id, |
463
|
1 |
|
'orders' => $orders, |
464
|
|
|
); |
465
|
|
|
|
466
|
1 |
|
// invoice may not be created if total is 0 |
467
|
|
|
if ($invoice instanceof \Model_Invoice && $invoice->status == \Model_Invoice::STATUS_UNPAID) { |
468
|
|
|
$result['invoice_hash'] = $invoice->hash; |
469
|
|
|
} |
470
|
1 |
|
|
471
|
|
|
return $result; |
472
|
|
|
} |
473
|
|
|
|
474
|
|
|
public function createFromCart(\Model_Client $client, $gateway_id = null) |
475
|
|
|
{ |
476
|
|
|
$cart = $this->getSessionCart(); |
477
|
|
|
$ca = $this->toApiArray($cart); |
478
|
|
|
if (count($ca['items']) == 0) { |
479
|
|
|
throw new \Box_Exception('Can not checkout empty cart.'); |
480
|
|
|
} |
481
|
|
|
|
482
|
|
|
|
483
|
|
|
$currency = $this->di['db']->getExistingModelById('Currency', $cart->currency_id, 'Currency not found.'); |
484
|
|
|
|
485
|
|
|
//set default client currency |
486
|
|
|
if (!$client->currency) { |
487
|
|
|
$client->currency = $currency->code; |
488
|
|
|
$this->di['db']->store($client); |
489
|
|
|
} |
490
|
|
|
|
491
|
|
|
if ($client->currency != $currency->code) { |
492
|
|
|
throw new \Box_Exception('Selected currency :selected does not match your profile currency :code. Please change cart currency to continue.', |
493
|
|
|
array(':selected' => $currency->code, ':code' => $client->currency)); |
494
|
|
|
} |
495
|
|
|
|
496
|
|
|
$clientService = $this->di['mod_service']('client'); |
497
|
|
|
$taxed = $clientService->isClientTaxable($client); |
498
|
|
|
|
499
|
|
|
$orders = array(); |
500
|
|
|
$invoice_items = array(); |
501
|
|
|
$master_order = null; |
502
|
|
|
$i = 0; |
503
|
|
|
|
504
|
|
|
foreach ($this->getCartProducts($cart) as $p) { |
505
|
|
|
$item = $this->cartProductToApiArray($p); |
506
|
|
|
|
507
|
|
|
$order = $this->di['db']->dispense('ClientOrder'); |
508
|
|
|
$order->client_id = $client->id; |
509
|
|
|
$order->promo_id = $cart->promo_id; |
510
|
|
|
$order->product_id = $item['product_id']; |
511
|
|
|
$order->form_id = $item['form_id']; |
512
|
|
|
|
513
|
|
|
$order->group_id = $cart->id; |
514
|
|
|
$order->group_master = ($i == 0); |
515
|
|
|
$order->invoice_option = 'issue-invoice'; |
516
|
|
|
$order->title = $item['title']; |
517
|
|
|
$order->currency = $currency->code; |
518
|
|
|
$order->service_type = $item['type']; |
519
|
|
|
$order->unit = $this->di['array_get']($item, 'unit', NULL); |
520
|
|
|
$order->period = $this->di['array_get']($item, 'period', NULL); |
521
|
|
|
$order->quantity = $this->di['array_get']($item, 'quantity', NULL); |
522
|
|
|
$order->price = $item['price'] * $currency->conversion_rate; |
523
|
|
|
$order->discount = $item['discount_price'] * $currency->conversion_rate; |
524
|
|
|
$order->status = \Model_ClientOrder::STATUS_PENDING_SETUP; |
525
|
|
|
$order->notes = $this->di['array_get']($item, 'notes', NULL); |
526
|
|
|
$order->config = json_encode($item); |
527
|
|
|
$order->created_at = date('Y-m-d H:i:s'); |
528
|
|
|
$order->updated_at = date('Y-m-d H:i:s'); |
529
|
|
|
$this->di['db']->store($order); |
530
|
|
|
|
531
|
|
|
$orders[] = $order; |
532
|
|
|
|
533
|
|
|
// mark promo as used |
534
|
|
|
if ($cart->promo_id) { |
535
|
|
|
$promo = $this->di['db']->getExistingModelById('Promo', $cart->promo_id, 'Promo not found.'); |
536
|
|
|
$this->usePromo($promo); |
537
|
|
|
|
538
|
|
|
//set promo info for later use |
539
|
|
|
$order->promo_recurring = $promo->recurring; |
540
|
|
|
$order->promo_used = 1; |
541
|
|
|
$this->di['db']->store($order); |
542
|
|
|
} |
543
|
|
|
|
544
|
|
|
$orderService = $this->di['mod_service']('order'); |
545
|
|
|
$orderService->saveStatusChange($order, 'Order created'); |
546
|
|
|
|
547
|
|
|
$invoice_items[] = array( |
548
|
|
|
'title' => $order->title, |
549
|
|
|
'price' => $order->price, |
550
|
|
|
'quantity' => $order->quantity, |
551
|
|
|
'unit' => $order->unit, |
552
|
|
|
'period' => $order->period, |
553
|
|
|
'taxed' => $taxed, |
554
|
|
|
'type' => \Model_InvoiceItem::TYPE_ORDER, |
555
|
|
|
'rel_id' => $order->id, |
556
|
|
|
'task' => \Model_InvoiceItem::TASK_ACTIVATE, |
557
|
|
|
); |
558
|
|
|
|
559
|
|
|
if($order->discount > 0){ |
560
|
|
|
$invoice_items[] = array( |
561
|
|
|
'title' => __('Discount: :product', array(':product' => $order->title)), |
562
|
|
|
'price' => $order->discount * -1, |
563
|
|
|
'quantity' => 1, |
564
|
|
|
'unit' => 'discount', |
565
|
|
|
'rel_id' => $order->id, |
566
|
|
|
'taxed' => $taxed, |
567
|
|
|
); |
568
|
|
|
} |
569
|
|
|
|
570
|
|
|
if ($item['setup_price'] > 0) { |
571
|
|
|
$setup_price = ($item['setup_price'] * $currency->conversion_rate) - ($item['discount_setup'] * $currency->conversion_rate); |
572
|
|
|
$invoice_items[] = array( |
573
|
|
|
'title' => __(':product setup', array(':product' => $order->title)), |
574
|
|
|
'price' => $setup_price, |
575
|
|
|
'quantity' => 1, |
576
|
|
|
'unit' => 'service', |
577
|
|
|
'taxed' => $taxed, |
578
|
|
|
); |
579
|
|
|
} |
580
|
|
|
|
581
|
|
|
//define master order to be returned |
582
|
|
|
if (null === $master_order) { |
583
|
|
|
$master_order = $order; |
584
|
|
|
} |
585
|
|
|
|
586
|
|
|
$i++; |
587
|
|
|
} |
588
|
|
|
|
589
|
|
|
if ($ca['total'] > 0) { //crete invoice if order total > 0 |
590
|
|
|
|
591
|
|
|
$invoiceService = $this->di['mod_service']('Invoice'); |
592
|
|
|
$invoiceModel = $invoiceService->prepareInvoice($client, array('client_id' => $client->id, 'items' => $invoice_items, 'gateway_id' => $gateway_id)); |
593
|
|
|
|
594
|
|
|
$clientBalanceService = $this->di['mod_service']('Client', 'Balance'); |
595
|
|
|
$balanceAmount = $clientBalanceService->getClientBalance($client); |
596
|
|
|
$useCredits = $balanceAmount >= $ca['total']; |
597
|
|
|
|
598
|
|
|
$invoiceService->approveInvoice($invoiceModel, array('id' => $invoiceModel->id, 'use_credits' => $useCredits)); |
599
|
|
|
|
600
|
|
|
if ($invoiceModel->status == \Model_Invoice::STATUS_UNPAID) { |
601
|
|
|
foreach ($orders as $order) { |
602
|
|
|
$order->unpaid_invoice_id = $invoiceModel->id; |
603
|
|
|
$this->di['db']->store($order); |
604
|
|
|
} |
605
|
|
|
} |
606
|
|
|
} |
607
|
|
|
|
608
|
|
|
//activate orders if product is setup to be activated after order place or order total is $0 |
609
|
|
|
$orderService = $this->di['mod_service']('Order'); |
610
|
|
|
$ids = array(); |
611
|
|
|
foreach ($orders as $order) { |
612
|
|
|
$ids[] = $order->id; |
613
|
|
|
$oa = $orderService->toApiArray($order, false, $client); |
614
|
|
|
$product = $this->di['db']->getExistingModelById('Product', $oa['product_id']); |
615
|
|
|
try { |
616
|
|
|
if ($product->setup == \Model_ProductTable::SETUP_AFTER_ORDER){ |
617
|
|
|
$orderService->activateOrder($order); |
618
|
|
|
} |
619
|
|
|
|
620
|
|
|
if ($ca['total'] <= 0 && $product->setup == \Model_ProductTable::SETUP_AFTER_PAYMENT && $oa['total'] - $oa['discount'] <= 0){ |
621
|
|
|
$orderService->activateOrder($order); |
622
|
|
|
} |
623
|
|
|
|
624
|
|
|
if ($ca['total'] > 0 && $product->setup == \Model_ProductTable::SETUP_AFTER_PAYMENT && $invoiceModel->status == \Model_Invoice::STATUS_PAID ){ |
625
|
|
|
$orderService->activateOrder($order); |
626
|
|
|
} |
627
|
|
|
} |
628
|
|
|
catch (\Exception $e) { |
629
|
|
|
error_log($e->getMessage()); |
630
|
|
|
$status = 'error'; |
631
|
1 |
|
$notes = 'Order could not be activated after checkout due to error: ' . $e->getMessage(); |
632
|
|
|
$orderService->orderStatusAdd($order, $status, $notes); |
633
|
1 |
|
} |
634
|
1 |
|
} |
635
|
1 |
|
|
636
|
1 |
|
return array( |
637
|
|
|
$master_order, |
638
|
1 |
|
isset($invoiceModel) ? $invoiceModel : null, |
639
|
|
|
$ids, |
640
|
1 |
|
); |
641
|
|
|
} |
642
|
|
|
|
643
|
|
|
public function usePromo(\Model_Promo $promo) |
644
|
|
|
{ |
645
|
|
|
$promo->used++; |
646
|
|
|
$promo->updated_at = date('Y-m-d H:i:s'); |
647
|
|
|
$this->di['db']->store($promo); |
648
|
|
|
} |
649
|
|
|
|
650
|
|
|
public function findActivePromoByCode($code) |
651
|
|
|
{ |
652
|
|
|
return $this->di['db']->findOne('Promo', 'code = :code AND active = 1 ORDER BY id ASC', array(':code' => $code)); |
653
|
|
|
} |
654
|
|
|
|
655
|
|
|
private function getItemPrice(\Model_CartProduct $model) |
|
|
|
|
656
|
|
|
{ |
657
|
|
|
$product = $this->di['db']->load('Product', $model->product_id); |
658
|
|
|
$config = $this->getItemConfig($model); |
659
|
|
|
$repo = $product->getTable(); |
660
|
|
|
return $repo->getProductPrice($product, $config); |
661
|
|
|
} |
662
|
|
|
|
663
|
|
|
private function getItemSetupPrice(\Model_CartProduct $model) |
|
|
|
|
664
|
|
|
{ |
665
|
|
|
$product = $this->di['db']->load('Product', $model->product_id); |
666
|
|
|
$config = $this->getItemConfig($model); |
667
|
|
|
$repo = $product->getTable(); |
668
|
|
|
return $repo->getProductSetupPrice($product, $config); |
669
|
|
|
} |
670
|
|
|
|
671
|
|
|
/** |
672
|
|
|
* Function checks if product is related to other products in cart |
673
|
|
|
* If relation exists then count discount for this |
674
|
|
|
* |
675
|
|
|
* @param \Model_Cart $cart |
676
|
|
|
* @param \Model_CartProduct $model |
677
|
|
|
* @return number |
678
|
|
|
*/ |
679
|
|
|
protected function getRelatedItemsDiscount(\Model_Cart $cart, \Model_CartProduct $model) |
680
|
|
|
{ |
681
|
|
|
$product = $this->di['db']->load('Product', $model->product_id); |
682
|
|
|
$repo = $product->getTable(); |
683
|
|
|
$config = $this->getItemConfig($model); |
684
|
|
|
|
685
|
|
|
$discount = 0; |
686
|
|
|
if(method_exists($repo, 'getRelatedDiscount')) { |
687
|
|
|
$list = array(); |
688
|
|
|
$products = $this->getCartProducts($cart); |
689
|
|
|
foreach($products as $p) { |
690
|
|
|
$item = $this->di['db']->toArray($p); |
691
|
|
|
$item['config'] = $this->getItemConfig($p); |
692
|
|
|
$list[] = $item; |
693
|
|
|
} |
694
|
|
|
$discount = $repo->getRelatedDiscount($list, $product, $config); |
695
|
|
|
} |
696
|
|
|
return $discount; |
697
|
|
|
} |
698
|
|
|
|
699
|
|
|
private function getItemTitle(\Model_CartProduct $model) |
700
|
|
|
{ |
701
|
|
|
$product = $this->di['db']->load('Product', $model->product_id); |
702
|
|
|
$config = $this->getItemConfig($model); |
703
|
|
|
$service = $product->getService(); |
704
|
|
|
if(method_exists($service, 'getCartProductTitle')) { |
705
|
|
|
return $service->getCartProductTitle($product, $config); |
706
|
|
|
} else { |
707
|
|
|
return __(':product_title', array(':product_title'=>$product->title)); |
708
|
|
|
} |
709
|
|
|
} |
710
|
|
|
|
711
|
|
|
protected function getItemPromoDiscount(\Model_CartProduct $model, \Model_Promo $promo) |
712
|
|
|
{ |
713
|
|
|
$product = $this->di['db']->load('Product', $model->product_id); |
714
|
|
|
$repo = $this->di['mod_service']('product'); |
715
|
|
|
$config = $this->getItemConfig($model); |
716
|
|
|
return $repo->getProductDiscount($product, $promo, $config); |
717
|
|
|
} |
718
|
|
|
|
719
|
|
|
public function getItemConfig(\Model_CartProduct $model) |
720
|
|
|
{ |
721
|
|
|
return $this->di['tools']->decodeJ($model->config); |
722
|
|
|
} |
723
|
|
|
|
724
|
|
|
public function cartProductToApiArray(\Model_CartProduct $model) |
725
|
|
|
{ |
726
|
|
|
$product = $this->di['db']->load('Product', $model->product_id); |
727
|
|
|
$repo = $product->getTable(); |
728
|
|
|
$config = $this->getItemConfig($model); |
729
|
|
|
$setup = $repo->getProductSetupPrice($product, $config); |
730
|
|
|
$price = $repo->getProductPrice($product, $config); |
731
|
|
|
$qty = $this->di['array_get']($config, 'quantity', 1) ; |
732
|
|
|
|
733
|
|
|
list ($discount_price, $discount_setup) = $this->getProductDiscount($model, $setup); |
734
|
|
|
|
735
|
|
|
$discount_total = $discount_price + $discount_setup; |
736
|
|
|
|
737
|
|
|
$subtotal = ($price * $qty); |
738
|
|
|
if(abs($discount_total) > ($subtotal + $setup) ) { |
739
|
|
|
$discount_total = $subtotal; |
740
|
|
|
$discount_price = $subtotal; |
741
|
|
|
} |
742
|
|
|
|
743
|
|
|
$data = array_merge($config, array( |
744
|
|
|
'id' => $model->id, |
745
|
|
|
'product_id' => $product->id, |
746
|
|
|
'form_id' => $product->form_id, |
747
|
|
|
'title' => $this->getItemTitle($model), |
748
|
|
|
'type' => $product->type, |
749
|
3 |
|
'quantity' => $qty, |
750
|
|
|
'unit' => $repo->getUnit($product), |
751
|
3 |
|
'price' => $price, |
752
|
3 |
|
'setup_price' => $setup, |
753
|
3 |
|
'discount' => $discount_total, |
754
|
3 |
|
'discount_price'=> $discount_price, |
755
|
2 |
|
'discount_setup'=> $discount_setup, |
756
|
2 |
|
'total' => $subtotal, |
757
|
|
|
)); |
758
|
2 |
|
return $data; |
759
|
1 |
|
} |
760
|
1 |
|
|
761
|
2 |
|
public function getProductDiscount(\Model_CartProduct $cartProduct, $setup) |
762
|
3 |
|
{ |
763
|
|
|
$cart = $this->di['db']->load('Cart', $cartProduct->cart_id); |
764
|
|
|
$discount_price = $this->getRelatedItemsDiscount($cart, $cartProduct); |
765
|
|
|
$discount_setup = 0; // discount for setup price |
766
|
|
|
if($cart->promo_id) { |
767
|
|
|
$promo = $this->di['db']->getExistingModelById('Promo', $cart->promo_id, 'Promo not found'); |
768
|
|
|
//Promo discount should override related item discount |
769
|
|
|
$discount_price = $this->getItemPromoDiscount($cartProduct, $promo); |
770
|
|
|
|
771
|
|
|
if($promo->freesetup) { |
772
|
|
|
$discount_setup = $setup; |
773
|
|
|
} |
774
|
|
|
} |
775
|
|
|
return array($discount_price, $discount_setup); |
776
|
|
|
} |
777
|
|
|
} |