Passed
Push — master ( 410311...b19200 )
by Aimeos
05:40
created

Standard::getContext()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * @license LGPLv3, https://opensource.org/licenses/LGPL-3.0
5
 * @copyright Metaways Infosystems GmbH, 2014
6
 * @copyright Aimeos (aimeos.org), 2015-2021
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 ) : \Aimeos\MShop\Order\Item\Iface
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 ) : \Aimeos\MShop\Order\Item\Iface
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 ) : \Aimeos\MShop\Order\Item\Iface
110
	{
111
		switch( $orderItem->getStatusPayment() )
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
	 * @return \Aimeos\Controller\Common\Order\Iface Order controller for fluent interface
138
	 */
139
	protected function addStatusItem( string $parentid, string $type, string $value ) : Iface
140
	{
141
		$manager = \Aimeos\MShop::create( $this->getContext(), 'order/status' );
142
143
		$item = $manager->create();
144
		$item->setParentId( $parentid );
145
		$item->setType( $type );
146
		$item->setValue( $value );
147
148
		$manager->save( $item, false );
149
150
		return $this;
151
	}
152
153
154
	/**
155
	 * Returns the product articles and their bundle product codes for the given article ID
156
	 *
157
	 * @param string $prodId Product ID of the article whose stock level changed
158
	 * @return array Associative list of article codes as keys and lists of bundle product codes as values
159
	 */
160
	protected function getBundleMap( string $prodId ) : array
161
	{
162
		$bundleMap = [];
163
		$productManager = \Aimeos\MShop::create( $this->context, 'product' );
164
165
		$search = $productManager->filter();
166
		$func = $search->make( 'product:has', ['product', 'default', $prodId] );
167
		$expr = array(
168
			$search->compare( '==', 'product.type', 'bundle' ),
169
			$search->compare( '!=', $func, null ),
170
		);
171
		$search->setConditions( $search->and( $expr ) );
172
		$search->slice( 0, 0x7fffffff );
173
174
		$bundleItems = $productManager->search( $search, array( 'product' ) );
175
176
		foreach( $bundleItems as $bundleItem )
177
		{
178
			foreach( $bundleItem->getRefItems( 'product', null, 'default' ) as $item ) {
179
				$bundleMap[$item->getId()][] = $bundleItem->getId();
180
			}
181
		}
182
183
		return $bundleMap;
184
	}
185
186
187
	/**
188
	 * Returns the context item object.
189
	 *
190
	 * @return \Aimeos\MShop\Context\Item\Iface Context item object
191
	 */
192
	protected function getContext() : \Aimeos\MShop\Context\Item\Iface
193
	{
194
		return $this->context;
195
	}
196
197
198
	/**
199
	 * Returns the last status item for the given order ID.
200
	 *
201
	 * @param string $parentid Order ID
202
	 * @param string $type Status type constant
203
	 * @param string $status New status value stored along with the order item
204
	 * @return \Aimeos\MShop\Order\Item\Status\Iface|null Order status item or NULL if no item is available
205
	 */
206
	protected function getLastStatusItem( string $parentid, string $type, string $status ) : ?\Aimeos\MShop\Order\Item\Status\Iface
207
	{
208
		$manager = \Aimeos\MShop::create( $this->getContext(), 'order/status' );
209
210
		$search = $manager->filter();
211
		$expr = array(
212
			$search->compare( '==', 'order.status.parentid', $parentid ),
213
			$search->compare( '==', 'order.status.type', $type ),
214
			$search->compare( '==', 'order.status.value', $status ),
215
		);
216
		$search->setConditions( $search->and( $expr ) );
217
		$search->setSortations( array( $search->sort( '-', 'order.status.ctime' ) ) );
218
		$search->slice( 0, 1 );
219
220
		return $manager->search( $search )->first();
221
	}
222
223
224
	/**
225
	 * Returns the stock items for the given product codes
226
	 *
227
	 * @param iterable $prodIds List of product codes
228
	 * @param string $stockType Stock type code the stock items must belong to
229
	 * @return \Aimeos\Map Associative list of \Aimeos\MShop\Stock\Item\Iface and IDs as values
230
	 */
231
	protected function getStockItems( iterable $prodIds, string $stockType ) : \Aimeos\Map
232
	{
233
		$stockManager = \Aimeos\MShop::create( $this->context, 'stock' );
234
235
		$search = $stockManager->filter()->slice( 0, count( $prodIds ) )
236
			->add( ['stock.productid' => $prodIds, 'stock.type' => $stockType] );
237
238
		return $stockManager->search( $search );
239
	}
240
241
242
	/**
243
	 * Increases or decreses the coupon code counts referenced in the order by the given value.
244
	 *
245
	 * @param \Aimeos\MShop\Order\Item\Iface $orderItem Order item object
246
	 * @param int $how Positive or negative integer number for increasing or decreasing the coupon count
247
	 * @return \Aimeos\Controller\Common\Order\Iface Order controller for fluent interface
248
	 */
249
	protected function updateCoupons( \Aimeos\MShop\Order\Item\Iface $orderItem, int $how = +1 )
250
	{
251
		$context = $this->getContext();
252
		$manager = \Aimeos\MShop::create( $context, 'order/base/coupon' );
253
		$couponCodeManager = \Aimeos\MShop::create( $context, 'coupon/code' );
254
255
		$search = $manager->filter();
256
		$search->setConditions( $search->compare( '==', 'order.base.coupon.baseid', $orderItem->getBaseId() ) );
257
258
		$start = 0;
259
260
		$couponCodeManager->begin();
261
262
		try
263
		{
264
			do
265
			{
266
				$items = $manager->search( $search );
267
268
				foreach( $items as $item ) {
269
					$couponCodeManager->decrease( $item->getCode(), $how * -1 );
270
				}
271
272
				$count = count( $items );
273
				$start += $count;
274
				$search->slice( $start );
275
			}
276
			while( $count >= $search->getLimit() );
277
278
			$couponCodeManager->commit();
279
		}
280
		catch( \Exception $e )
281
		{
282
			$couponCodeManager->rollback();
283
			throw $e;
284
		}
285
286
		return $this;
287
	}
288
289
290
	/**
291
	 * Increases or decreases the stock level or the coupon code count for referenced items of the given order.
292
	 *
293
	 * @param \Aimeos\MShop\Order\Item\Iface $orderItem Order item object
294
	 * @param string $type Constant from \Aimeos\MShop\Order\Item\Status\Base, e.g. STOCK_UPDATE or COUPON_UPDATE
295
	 * @param string $status New status value stored along with the order item
296
	 * @param int $value Number to increse or decrease the stock level or coupon code count
297
	 * @return \Aimeos\Controller\Common\Order\Iface Order controller for fluent interface
298
	 */
299
	protected function updateStatus( \Aimeos\MShop\Order\Item\Iface $orderItem, string $type, string $status, int $value )
300
	{
301
		$statusItem = $this->getLastStatusItem( $orderItem->getId(), $type, $status );
0 ignored issues
show
Bug introduced by
It seems like $orderItem->getId() can also be of type null; however, parameter $parentid of Aimeos\Controller\Common...rd::getLastStatusItem() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

301
		$statusItem = $this->getLastStatusItem( /** @scrutinizer ignore-type */ $orderItem->getId(), $type, $status );
Loading history...
302
303
		if( $statusItem && $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
		return $this->addStatusItem( $orderItem->getId(), $type, $status );
0 ignored issues
show
Bug introduced by
It seems like $orderItem->getId() can also be of type null; however, parameter $parentid of Aimeos\Controller\Common...andard::addStatusItem() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

313
		return $this->addStatusItem( /** @scrutinizer ignore-type */ $orderItem->getId(), $type, $status );
Loading history...
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 int $how Positive or negative integer number for increasing or decreasing the stock levels
322
	 * @return \Aimeos\Controller\Common\Order\Iface Order controller for fluent interface
323
	 */
324
	protected function updateStock( \Aimeos\MShop\Order\Item\Iface $orderItem, int $how = +1 )
325
	{
326
		$context = $this->getContext();
327
		$stockManager = \Aimeos\MShop::create( $context, 'stock' );
328
		$manager = \Aimeos\MShop::create( $context, 'order/base/product' );
329
330
		$search = $manager->filter();
331
		$search->setConditions( $search->compare( '==', 'order.base.product.baseid', $orderItem->getBaseId() ) );
332
333
		$start = 0;
334
335
		$stockManager->begin();
336
337
		try
338
		{
339
			do
340
			{
341
				$items = $manager->search( $search );
342
343
				foreach( $items as $item )
344
				{
345
					$stockManager->decrease( [$item->getProductId() => -1 * $how * $item->getQuantity()], $item->getStockType() );
346
347
					switch( $item->getType() ) {
348
						case 'default':
349
							$this->updateStockBundle( $item->getParentProductId(), $item->getStockType() ); break;
350
						case 'select':
351
							$this->updateStockSelection( $item->getParentProductId(), $item->getStockType() ); break;
352
					}
353
				}
354
355
				$count = count( $items );
356
				$start += $count;
357
				$search->slice( $start );
358
			}
359
			while( $count >= $search->getLimit() );
360
361
			$stockManager->commit();
362
		}
363
		catch( \Exception $e )
364
		{
365
			$stockManager->rollback();
366
			throw $e;
367
		}
368
369
		return $this;
370
	}
371
372
373
	/**
374
	 * Updates the stock levels of bundles for a specific type
375
	 *
376
	 * @param string $prodId Unique product ID
377
	 * @param string $stockType Unique stock type
378
	 * @return \Aimeos\Controller\Common\Order\Iface Order controller for fluent interface
379
	 */
380
	protected function updateStockBundle( string $prodId, string $stockType )
381
	{
382
		if( ( $bundleMap = $this->getBundleMap( $prodId ) ) === [] ) {
383
			return;
384
		}
385
386
387
		$bundleIds = $stock = [];
388
389
		foreach( $this->getStockItems( array_keys( $bundleMap ), $stockType ) as $stockItem )
390
		{
391
			if( isset( $bundleMap[$stockItem->getProductId()] ) && $stockItem->getStockLevel() !== null )
392
			{
393
				foreach( $bundleMap[$stockItem->getProductId()] as $bundleId )
394
				{
395
					if( isset( $stock[$bundleId] ) ) {
396
						$stock[$bundleId] = min( $stock[$bundleId], $stockItem->getStockLevel() );
397
					} else {
398
						$stock[$bundleId] = $stockItem->getStockLevel();
399
					}
400
401
					$bundleIds[$bundleId] = null;
402
				}
403
			}
404
		}
405
406
		if( empty( $stock ) ) {
407
			return;
408
		}
409
410
		$stockManager = \Aimeos\MShop::create( $this->context, 'stock' );
411
412
		foreach( $this->getStockItems( array_keys( $bundleIds ), $stockType ) as $item )
413
		{
414
			if( isset( $stock[$item->getProductId()] ) )
415
			{
416
				$item->setStockLevel( $stock[$item->getProductId()] );
417
				$stockManager->save( $item );
418
			}
419
		}
420
421
		return $this;
422
	}
423
424
425
	/**
426
	 * Updates the stock levels of selection products for a specific type
427
	 *
428
	 * @param string $prodId Unique product ID
429
	 * @param string $stocktype Unique stock type
430
	 * @return \Aimeos\Controller\Common\Order\Iface Order controller for fluent interface
431
	 */
432
	protected function updateStockSelection( string $prodId, string $stocktype )
433
	{
434
		$stockManager = \Aimeos\MShop::create( $this->context, 'stock' );
435
		$productManager = \Aimeos\MShop::create( $this->context, 'product' );
436
437
		$sum = 0; $selStockItem = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $selStockItem is dead and can be removed.
Loading history...
Unused Code introduced by
The assignment to $sum is dead and can be removed.
Loading history...
438
		$productItem = $productManager->get( $prodId, ['product'] );
439
		$prodIds = $productItem->getRefItems( 'product', 'default', 'default' )->getId()->push( $productItem->getId() );
440
441
		$stockItems = $this->getStockItems( $prodIds, $stocktype )->col( null, 'stock.productid' );
442
		$selStockItem = $stockItems->pull( $prodId ) ?: $stockManager->create();
443
444
		$sum = $stockItems->getStockLevel()->reduce( function( $result, $value ) {
445
			return $result !== null && $value !== null ? $result + $value : null;
446
		}, 0 );
447
448
		$selStockItem->setProductId( $productItem->getId() )->setType( $stocktype )->setStockLevel( $sum );
449
		$stockManager->save( $selStockItem, false );
450
451
		return $this;
452
	}
453
}
454