Passed
Push — master ( 8f16ed...3311dc )
by Aimeos
10:04
created

common/src/Controller/Common/Order/Standard.php (4 issues)

Labels
Severity
1
<?php
2
3
/**
4
 * @license LGPLv3, http://opensource.org/licenses/LGPL-3.0
5
 * @copyright Metaways Infosystems GmbH, 2014
6
 * @copyright Aimeos (aimeos.org), 2015-2018
7
 * @package Controller
8
 * @subpackage Common
9
 */
10
11
12
namespace Aimeos\Controller\Common\Order;
13
14
15
/**
16
 * Common order controller methods.
17
 *
18
 * @package Controller
19
 * @subpackage Common
20
 */
21
class Standard
22
	implements \Aimeos\Controller\Common\Order\Iface
23
{
24
	private $context;
25
26
27
	/**
28
	 * Initializes the object.
29
	 *
30
	 * @param \Aimeos\MShop\Context\Item\Iface $context
31
	 */
32
	public function __construct( \Aimeos\MShop\Context\Item\Iface $context )
33
	{
34
		$this->context = $context;
35
	}
36
37
38
	/**
39
	 * Blocks the resources listed in the order.
40
	 *
41
	 * Every order contains resources like products or redeemed coupon codes
42
	 * that must be blocked so they can't be used by another customer in a
43
	 * later order. This method reduces the the stock level of products, the
44
	 * counts of coupon codes and others.
45
	 *
46
	 * It's save to call this method multiple times for one order. In this case,
47
	 * the actions will be executed only once. All subsequent calls will do
48
	 * nothing as long as the resources haven't been unblocked in the meantime.
49
	 *
50
	 * You can also block and unblock resources several times. Please keep in
51
	 * mind that unblocked resources may be reused by other orders in the
52
	 * meantime. This can lead to an oversell of products!
53
	 *
54
	 * @param \Aimeos\MShop\Order\Item\Iface $orderItem Order item object
55
	 * @return \Aimeos\MShop\Order\Item\Iface Order item object
56
	 */
57
	public function block( \Aimeos\MShop\Order\Item\Iface $orderItem )
58
	{
59
		$this->updateStatus( $orderItem, \Aimeos\MShop\Order\Item\Status\Base::STOCK_UPDATE, 1, -1 );
60
		$this->updateStatus( $orderItem, \Aimeos\MShop\Order\Item\Status\Base::COUPON_UPDATE, 1, -1 );
61
62
		return $orderItem;
63
	}
64
65
66
	/**
67
	 * Frees the resources listed in the order.
68
	 *
69
	 * If customers created orders but didn't pay for them, the blocked resources
70
	 * like products and redeemed coupon codes must be unblocked so they can be
71
	 * ordered again or used by other customers. This method increased the stock
72
	 * level of products, the counts of coupon codes and others.
73
	 *
74
	 * It's save to call this method multiple times for one order. In this case,
75
	 * the actions will be executed only once. All subsequent calls will do
76
	 * nothing as long as the resources haven't been blocked in the meantime.
77
	 *
78
	 * You can also unblock and block resources several times. Please keep in
79
	 * mind that unblocked resources may be reused by other orders in the
80
	 * meantime. This can lead to an oversell of products!
81
	 *
82
	 * @param \Aimeos\MShop\Order\Item\Iface $orderItem Order item object
83
	 * @return \Aimeos\MShop\Order\Item\Iface Order item object
84
	 */
85
	public function unblock( \Aimeos\MShop\Order\Item\Iface $orderItem )
86
	{
87
		$this->updateStatus( $orderItem, \Aimeos\MShop\Order\Item\Status\Base::STOCK_UPDATE, 0, +1 );
88
		$this->updateStatus( $orderItem, \Aimeos\MShop\Order\Item\Status\Base::COUPON_UPDATE, 0, +1 );
89
90
		return $orderItem;
91
	}
92
93
94
	/**
95
	 * Blocks or frees the resources listed in the order if necessary.
96
	 *
97
	 * After payment status updates, the resources like products or coupon
98
	 * codes listed in the order must be blocked or unblocked. This method
99
	 * cares about executing the appropriate action depending on the payment
100
	 * status.
101
	 *
102
	 * It's save to call this method multiple times for one order. In this case,
103
	 * the actions will be executed only once. All subsequent calls will do
104
	 * nothing as long as the payment status hasn't changed in the meantime.
105
	 *
106
	 * @param \Aimeos\MShop\Order\Item\Iface $orderItem Order item object
107
	 * @return \Aimeos\MShop\Order\Item\Iface Order item object
108
	 */
109
	public function update( \Aimeos\MShop\Order\Item\Iface $orderItem )
110
	{
111
		switch( $orderItem->getPaymentStatus() )
112
		{
113
			case \Aimeos\MShop\Order\Item\Base::PAY_DELETED:
114
			case \Aimeos\MShop\Order\Item\Base::PAY_CANCELED:
115
			case \Aimeos\MShop\Order\Item\Base::PAY_REFUSED:
116
			case \Aimeos\MShop\Order\Item\Base::PAY_REFUND:
117
				$this->unblock( $orderItem );
118
				break;
119
120
			case \Aimeos\MShop\Order\Item\Base::PAY_PENDING:
121
			case \Aimeos\MShop\Order\Item\Base::PAY_AUTHORIZED:
122
			case \Aimeos\MShop\Order\Item\Base::PAY_RECEIVED:
123
				$this->block( $orderItem );
124
				break;
125
		}
126
127
		return $orderItem;
128
	}
129
130
131
	/**
132
	 * Adds a new status record to the order with the type and value.
133
	 *
134
	 * @param string $parentid Order ID
135
	 * @param string $type Status type
136
	 * @param string $value Status value
137
	 */
138
	protected function addStatusItem( $parentid, $type, $value )
139
	{
140
		$manager = \Aimeos\MShop::create( $this->getContext(), 'order/status' );
141
142
		$item = $manager->createItem();
143
		$item->setParentId( $parentid );
0 ignored issues
show
The method setParentId() does not exist on Aimeos\MShop\Attribute\Item\Iface. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

143
		$item->/** @scrutinizer ignore-call */ 
144
         setParentId( $parentid );

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
144
		$item->setType( $type );
145
		$item->setValue( $value );
0 ignored issues
show
The method setValue() does not exist on Aimeos\MShop\Attribute\Item\Iface. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

145
		$item->/** @scrutinizer ignore-call */ 
146
         setValue( $value );

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
146
147
		$manager->saveItem( $item, false );
148
	}
149
150
151
	/**
152
	 * Returns the product articles and their bundle product codes for the given article ID
153
	 *
154
	 * @param string $prodId Product ID of the article whose stock level changed
155
	 * @return array Associative list of article codes as keys and lists of bundle product codes as values
156
	 */
157
	protected function getBundleMap( $prodId )
158
	{
159
		$bundleMap = [];
160
		$productManager = \Aimeos\MShop::create( $this->context, 'product' );
161
162
		$search = $productManager->createSearch();
163
		$func = $search->createFunction( 'product:has', ['product', 'default', $prodId] );
164
		$expr = array(
165
			$search->compare( '==', 'product.type', 'bundle' ),
166
			$search->compare( '!=', $func, null ),
167
		);
168
		$search->setConditions( $search->combine( '&&', $expr ) );
169
		$search->setSlice( 0, 0x7fffffff );
170
171
		$bundleItems = $productManager->searchItems( $search, array( 'product' ) );
172
173
		foreach( $bundleItems as $bundleItem )
174
		{
175
			foreach( $bundleItem->getRefItems( 'product', null, 'default' ) as $item ) {
176
				$bundleMap[ $item->getCode() ][] = $bundleItem->getCode();
177
			}
178
		}
179
180
		return $bundleMap;
181
	}
182
183
184
	/**
185
	 * Returns the context item object.
186
	 *
187
	 * @return \Aimeos\MShop\Context\Item\Iface Context item object
188
	 */
189
	protected function getContext()
190
	{
191
		return $this->context;
192
	}
193
194
195
	/**
196
	 * Returns the last status item for the given order ID.
197
	 *
198
	 * @param string $parentid Order ID
199
	 * @param string $type Status type constant
200
	 * @param string $status New status value stored along with the order item
201
	 * @return \Aimeos\MShop\Order\Item\Status\Iface|false Order status item or false if no item is available
202
	 */
203
	protected function getLastStatusItem( $parentid, $type, $status )
204
	{
205
		$manager = \Aimeos\MShop::create( $this->getContext(), 'order/status' );
206
207
		$search = $manager->createSearch();
208
		$expr = array(
209
			$search->compare( '==', 'order.status.parentid', $parentid ),
210
			$search->compare( '==', 'order.status.type', $type ),
211
			$search->compare( '==', 'order.status.value', $status ),
212
		);
213
		$search->setConditions( $search->combine( '&&', $expr ) );
214
		$search->setSortations( array( $search->sort( '-', 'order.status.ctime' ) ) );
215
		$search->setSlice( 0, 1 );
216
217
		$result = $manager->searchItems( $search );
218
219
		return reset( $result );
220
	}
221
222
223
	/**
224
	 * Returns the stock items for the given product codes
225
	 *
226
	 * @param array $prodCodes List of product codes
227
	 * @param string $stockType Stock type code the stock items must belong to
228
	 * @return \Aimeos\MShop\Product\Item\Stock\Iface[] Associative list of stock IDs as keys and stock items as values
229
	 */
230
	protected function getStockItems( array $prodCodes, $stockType )
231
	{
232
		$stockManager = \Aimeos\MShop::create( $this->context, 'stock' );
233
234
		$search = $stockManager->createSearch();
235
		$expr = array(
236
			$search->compare( '==', 'stock.productcode', $prodCodes ),
237
			$search->compare( '==', 'stock.type', $stockType ),
238
		);
239
		$search->setConditions( $search->combine( '&&', $expr ) );
240
		$search->setSlice( 0, count( $prodCodes ) );
241
242
		return $stockManager->searchItems( $search );
243
	}
244
245
246
	/**
247
	 * Increases or decreses the coupon code counts referenced in the order by the given value.
248
	 *
249
	 * @param \Aimeos\MShop\Order\Item\Iface $orderItem Order item object
250
	 * @param integer $how Positive or negative integer number for increasing or decreasing the coupon count
251
	 */
252
	protected function updateCoupons( \Aimeos\MShop\Order\Item\Iface $orderItem, $how = +1 )
253
	{
254
		$context = $this->getContext();
255
		$manager = \Aimeos\MShop::create( $context, 'order/base/coupon' );
256
		$couponCodeManager = \Aimeos\MShop::create( $context, 'coupon/code' );
257
258
		$search = $manager->createSearch();
259
		$search->setConditions( $search->compare( '==', 'order.base.coupon.baseid', $orderItem->getBaseId() ) );
260
261
		$start = 0;
262
263
		$couponCodeManager->begin();
264
265
		try
266
		{
267
			do
268
			{
269
				$items = $manager->searchItems( $search );
270
271
				foreach( $items as $item ) {
272
					$couponCodeManager->decrease( $item->getCode(), $how * -1 );
273
				}
274
275
				$count = count( $items );
276
				$start += $count;
277
				$search->setSlice( $start );
278
			}
279
			while( $count >= $search->getSliceSize() );
280
281
			$couponCodeManager->commit();
282
		}
283
		catch( \Exception $e )
284
		{
285
			$couponCodeManager->rollback();
286
			throw $e;
287
		}
288
	}
289
290
291
	/**
292
	 * Increases or decreases the stock level or the coupon code count for referenced items of the given order.
293
	 *
294
	 * @param \Aimeos\MShop\Order\Item\Iface $orderItem Order item object
295
	 * @param string $type Constant from \Aimeos\MShop\Order\Item\Status\Base, e.g. STOCK_UPDATE or COUPON_UPDATE
296
	 * @param string $status New status value stored along with the order item
297
	 * @param integer $value Number to increse or decrease the stock level or coupon code count
298
	 */
299
	protected function updateStatus( \Aimeos\MShop\Order\Item\Iface $orderItem, $type, $status, $value )
300
	{
301
		$statusItem = $this->getLastStatusItem( $orderItem->getId(), $type, $status );
302
303
		if( $statusItem !== false && $statusItem->getValue() == $status ) {
304
			return;
305
		}
306
307
		if( $type == \Aimeos\MShop\Order\Item\Status\Base::STOCK_UPDATE ) {
308
			$this->updateStock( $orderItem, $value );
309
		} elseif( $type == \Aimeos\MShop\Order\Item\Status\Base::COUPON_UPDATE ) {
310
			$this->updateCoupons( $orderItem, $value );
311
		}
312
313
		$this->addStatusItem( $orderItem->getId(), $type, $status );
314
	}
315
316
317
	/**
318
	 * Increases or decreses the stock levels of the products referenced in the order by the given value.
319
	 *
320
	 * @param \Aimeos\MShop\Order\Item\Iface $orderItem Order item object
321
	 * @param integer $how Positive or negative integer number for increasing or decreasing the stock levels
322
	 */
323
	protected function updateStock( \Aimeos\MShop\Order\Item\Iface $orderItem, $how = +1 )
324
	{
325
		$context = $this->getContext();
326
		$stockManager = \Aimeos\MShop::create( $context, 'stock' );
327
		$manager = \Aimeos\MShop::create( $context, 'order/base/product' );
328
329
		$search = $manager->createSearch();
330
		$search->setConditions( $search->compare( '==', 'order.base.product.baseid', $orderItem->getBaseId() ) );
331
332
		$start = 0;
333
334
		$stockManager->begin();
335
336
		try
337
		{
338
			do
339
			{
340
				$items = $manager->searchItems( $search );
341
342
				foreach( $items as $item )
343
				{
344
					$stockManager->decrease( [$item->getProductCode() => $how * -1 * $item->getQuantity()], $item->getStockType() );
345
346
					switch( $item->getType() ) {
347
						case 'default':
348
							$this->updateStockBundle( $item->getProductId(), $item->getStockType() ); break;
349
						case 'select':
350
							$this->updateStockSelection( $item->getProductId(), $item->getStockType() ); break;
351
					}
352
				}
353
354
				$count = count( $items );
355
				$start += $count;
356
				$search->setSlice( $start );
357
			}
358
			while( $count >= $search->getSliceSize() );
359
360
			$stockManager->commit();
361
		}
362
		catch( \Exception $e )
363
		{
364
			$stockManager->rollback();
365
			throw $e;
366
		}
367
	}
368
369
370
	/**
371
	 * Updates the stock levels of bundles for a specific type
372
	 *
373
	 * @param string $prodId Unique product ID
374
	 * @param string $stockType Unique stock type
375
	 */
376
	protected function updateStockBundle( $prodId, $stockType )
377
	{
378
		if( ( $bundleMap = $this->getBundleMap( $prodId ) ) === [] ) {
379
			return;
380
		}
381
382
383
		$bundleCodes = $stock = [];
384
385
		foreach( $this->getStockItems( array_keys( $bundleMap ), $stockType ) as $stockItem )
386
		{
387
			if( isset( $bundleMap[$stockItem->getProductCode()] ) && $stockItem->getStockLevel() !== null )
388
			{
389
				foreach( $bundleMap[$stockItem->getProductCode()] as $bundleCode )
390
				{
391
					if( isset( $stock[$bundleCode] ) ) {
392
						$stock[$bundleCode] = min( $stock[$bundleCode], $stockItem->getStockLevel() );
393
					} else {
394
						$stock[$bundleCode] = $stockItem->getStockLevel();
395
					}
396
397
					$bundleCodes[$bundleCode] = null;
398
				}
399
			}
400
		}
401
402
		if( empty( $stock ) ) {
403
			return;
404
		}
405
406
		$stockManager = \Aimeos\MShop::create( $this->context, 'stock' );
407
408
		foreach( $this->getStockItems( array_keys( $bundleCodes ), $stockType ) as $item )
409
		{
410
			if( isset( $stock[$item->getProductCode()] ) )
411
			{
412
				$item->setStockLevel( $stock[$item->getProductCode()] );
413
				$stockManager->saveItem( $item );
414
			}
415
		}
416
	}
417
418
419
	/**
420
	 * Updates the stock levels of selection products for a specific type
421
	 *
422
	 * @param string $prodId Unique product ID
423
	 * @param string $stocktype Unique stock type
424
	 */
425
	protected function updateStockSelection( $prodId, $stocktype )
426
	{
427
		$productManager = \Aimeos\MShop::create( $this->context, 'product' );
428
		$stockManager = \Aimeos\MShop::create( $this->context, 'stock' );
429
430
		$productItem = $productManager->getItem( $prodId, array( 'product' ) );
431
		$prodCodes = array( $productItem->getCode() );
432
		$sum = 0; $selStockItem = null;
433
434
		foreach( $productItem->getRefItems( 'product', 'default', 'default' ) as $product ) {
435
			$prodCodes[] = $product->getCode();
436
		}
437
438
		foreach( $this->getStockItems( $prodCodes, $stocktype ) as $stockItem )
439
		{
440
			if( $stockItem->getProductCode() === $productItem->getCode() )
441
			{
442
				$selStockItem = $stockItem;
443
				continue;
444
			}
445
446
			if( ( $stock = $stockItem->getStockLevel() ) === null ) {
447
				$sum = null;
448
			}
449
450
			if( $sum !== null ) {
451
				$sum += $stock;
452
			}
453
		}
454
455
		if( $selStockItem === null )
456
		{
457
			$selStockItem = $stockManager->createItem();
458
			$selStockItem->setProductCode( $productItem->getCode() );
0 ignored issues
show
The method setProductCode() does not exist on Aimeos\MShop\Attribute\Item\Iface. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

458
			$selStockItem->/** @scrutinizer ignore-call */ 
459
                  setProductCode( $productItem->getCode() );

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
459
			$selStockItem->setType( $stocktype );
460
		}
461
462
		$selStockItem->setStockLevel( $sum );
0 ignored issues
show
The method setStockLevel() does not exist on Aimeos\MShop\Attribute\Item\Iface. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

462
		$selStockItem->/** @scrutinizer ignore-call */ 
463
                 setStockLevel( $sum );

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
463
		$stockManager->saveItem( $selStockItem, false );
464
	}
465
}
466