Passed
Push — master ( 8091ff...5b5d82 )
by Aimeos
05:28
created

Update::updateStock()   B

Complexity

Conditions 6
Paths 15

Size

Total Lines 46
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 26
dl 0
loc 46
rs 8.8817
c 0
b 0
f 0
cc 6
nc 15
nop 2
1
<?php
2
3
/**
4
 * @license LGPLv3, https://opensource.org/licenses/LGPL-3.0
5
 * @copyright Aimeos (aimeos.org), 2015-2023
6
 * @package MShop
7
 * @subpackage Order
8
 */
9
10
11
namespace Aimeos\MShop\Order\Manager;
12
13
14
/**
15
 * Update trait for order managers
16
 *
17
 * @package MShop
18
 * @subpackage Order
19
 */
20
trait Update
21
{
22
	/**
23
	 * Returns the context item object.
24
	 *
25
	 * @return \Aimeos\MShop\ContextIface Context item object
26
	 */
27
	abstract protected function context() : \Aimeos\MShop\ContextIface;
28
29
30
	/**
31
	 * Blocks the resources listed in the order.
32
	 *
33
	 * Every order contains resources like products or redeemed coupon codes
34
	 * that must be blocked so they can't be used by another customer in a
35
	 * later order. This method reduces the the stock level of products, the
36
	 * counts of coupon codes and others.
37
	 *
38
	 * It's save to call this method multiple times for one order. In this case,
39
	 * the actions will be executed only once. All subsequent calls will do
40
	 * nothing as long as the resources haven't been unblocked in the meantime.
41
	 *
42
	 * You can also block and unblock resources several times. Please keep in
43
	 * mind that unblocked resources may be reused by other orders in the
44
	 * meantime. This can lead to an oversell of products!
45
	 *
46
	 * @param \Aimeos\MShop\Order\Item\Iface $orderItem Order item object
47
	 * @return \Aimeos\MShop\Order\Item\Iface Order item object
48
	 */
49
	public function block( \Aimeos\MShop\Order\Item\Iface $orderItem ) : \Aimeos\MShop\Order\Item\Iface
50
	{
51
		$this->updateStatus( $orderItem, \Aimeos\MShop\Order\Item\Status\Base::STOCK_UPDATE, 1, -1 );
52
		$this->updateStatus( $orderItem, \Aimeos\MShop\Order\Item\Status\Base::COUPON_UPDATE, 1, -1 );
53
54
		return $orderItem;
55
	}
56
57
58
	/**
59
	 * Frees the resources listed in the order.
60
	 *
61
	 * If customers created orders but didn't pay for them, the blocked resources
62
	 * like products and redeemed coupon codes must be unblocked so they can be
63
	 * ordered again or used by other customers. This method increased the stock
64
	 * level of products, the counts of coupon codes and others.
65
	 *
66
	 * It's save to call this method multiple times for one order. In this case,
67
	 * the actions will be executed only once. All subsequent calls will do
68
	 * nothing as long as the resources haven't been blocked in the meantime.
69
	 *
70
	 * You can also unblock and block resources several times. Please keep in
71
	 * mind that unblocked resources may be reused by other orders in the
72
	 * meantime. This can lead to an oversell of products!
73
	 *
74
	 * @param \Aimeos\MShop\Order\Item\Iface $orderItem Order item object
75
	 * @return \Aimeos\MShop\Order\Item\Iface Order item object
76
	 */
77
	public function unblock( \Aimeos\MShop\Order\Item\Iface $orderItem ) : \Aimeos\MShop\Order\Item\Iface
78
	{
79
		$this->updateStatus( $orderItem, \Aimeos\MShop\Order\Item\Status\Base::STOCK_UPDATE, 0, +1 );
80
		$this->updateStatus( $orderItem, \Aimeos\MShop\Order\Item\Status\Base::COUPON_UPDATE, 0, +1 );
81
82
		return $orderItem;
83
	}
84
85
86
	/**
87
	 * Blocks or frees the resources listed in the order if necessary.
88
	 *
89
	 * After payment status updates, the resources like products or coupon
90
	 * codes listed in the order must be blocked or unblocked. This method
91
	 * cares about executing the appropriate action depending on the payment
92
	 * status.
93
	 *
94
	 * It's save to call this method multiple times for one order. In this case,
95
	 * the actions will be executed only once. All subsequent calls will do
96
	 * nothing as long as the payment status hasn't changed in the meantime.
97
	 *
98
	 * @param \Aimeos\MShop\Order\Item\Iface $orderItem Order item object
99
	 * @return \Aimeos\MShop\Order\Item\Iface Order item object
100
	 */
101
	public function update( \Aimeos\MShop\Order\Item\Iface $orderItem ) : \Aimeos\MShop\Order\Item\Iface
102
	{
103
		switch( $orderItem->getStatusPayment() )
104
		{
105
			case \Aimeos\MShop\Order\Item\Base::PAY_DELETED:
106
			case \Aimeos\MShop\Order\Item\Base::PAY_CANCELED:
107
			case \Aimeos\MShop\Order\Item\Base::PAY_REFUSED:
108
			case \Aimeos\MShop\Order\Item\Base::PAY_REFUND:
109
				$this->unblock( $orderItem );
110
				break;
111
112
			case \Aimeos\MShop\Order\Item\Base::PAY_PENDING:
113
			case \Aimeos\MShop\Order\Item\Base::PAY_AUTHORIZED:
114
			case \Aimeos\MShop\Order\Item\Base::PAY_RECEIVED:
115
				$this->block( $orderItem );
116
				break;
117
		}
118
119
		return $orderItem;
120
	}
121
122
123
	/**
124
	 * Adds a new status record to the order with the type and value.
125
	 *
126
	 * @param string $parentid Order ID
127
	 * @param string $type Status type
128
	 * @param string $value Status value
129
	 * @return \Aimeos\Controller\Common\Order\Iface Order controller for fluent interface
0 ignored issues
show
Bug introduced by
The type Aimeos\Controller\Common\Order\Iface was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
130
	 */
131
	protected function addStatusItem( string $parentid, string $type, string $value ) : Iface
132
	{
133
		$manager = \Aimeos\MShop::create( $this->context(), 'order/status' );
134
135
		$item = $manager->create();
136
		$item->setParentId( $parentid );
137
		$item->setType( $type );
138
		$item->setValue( $value );
139
140
		$manager->save( $item, false );
141
142
		return $this;
143
	}
144
145
146
	/**
147
	 * Returns the product articles and their bundle product codes for the given article ID
148
	 *
149
	 * @param string $prodId Product ID of the article whose stock level changed
150
	 * @return array Associative list of article codes as keys and lists of bundle product codes as values
151
	 */
152
	protected function getBundleMap( string $prodId ) : array
153
	{
154
		$bundleMap = [];
155
		$productManager = \Aimeos\MShop::create( $this->context(), 'product' );
156
157
		$search = $productManager->filter();
158
		$func = $search->make( 'product:has', ['product', 'default', $prodId] );
159
		$expr = array(
160
			$search->compare( '==', 'product.type', 'bundle' ),
161
			$search->compare( '!=', $func, null ),
162
		);
163
		$search->setConditions( $search->and( $expr ) );
164
		$search->slice( 0, 0x7fffffff );
165
166
		$bundleItems = $productManager->search( $search, array( 'product' ) );
167
168
		foreach( $bundleItems as $bundleItem )
169
		{
170
			foreach( $bundleItem->getRefItems( 'product', null, 'default' ) as $item ) {
171
				$bundleMap[$item->getId()][] = $bundleItem->getId();
172
			}
173
		}
174
175
		return $bundleMap;
176
	}
177
178
179
	/**
180
	 * Returns the last status item for the given order ID.
181
	 *
182
	 * @param string $parentid Order ID
183
	 * @param string $type Status type constant
184
	 * @param string $status New status value stored along with the order item
185
	 * @return \Aimeos\MShop\Order\Item\Status\Iface|null Order status item or NULL if no item is available
186
	 */
187
	protected function getLastStatusItem( string $parentid, string $type, string $status ) : ?\Aimeos\MShop\Order\Item\Status\Iface
188
	{
189
		$manager = \Aimeos\MShop::create( $this->context(), 'order/status' );
190
191
		$search = $manager->filter();
192
		$expr = array(
193
			$search->compare( '==', 'order.status.parentid', $parentid ),
194
			$search->compare( '==', 'order.status.type', $type ),
195
			$search->compare( '==', 'order.status.value', $status ),
196
		);
197
		$search->setConditions( $search->and( $expr ) );
198
		$search->setSortations( array( $search->sort( '-', 'order.status.ctime' ) ) );
199
		$search->slice( 0, 1 );
200
201
		return $manager->search( $search )->first();
202
	}
203
204
205
	/**
206
	 * Returns the stock items for the given product codes
207
	 *
208
	 * @param iterable $prodIds List of product codes
209
	 * @param string $stockType Stock type code the stock items must belong to
210
	 * @return \Aimeos\Map Associative list of \Aimeos\MShop\Stock\Item\Iface and IDs as values
211
	 */
212
	protected function getStockItems( iterable $prodIds, string $stockType ) : \Aimeos\Map
213
	{
214
		$stockManager = \Aimeos\MShop::create( $this->context(), 'stock' );
215
216
		$search = $stockManager->filter()->slice( 0, count( $prodIds ) )
217
			->add( ['stock.productid' => $prodIds, 'stock.type' => $stockType] );
218
219
		return $stockManager->search( $search );
220
	}
221
222
223
	/**
224
	 * Increases or decreses the coupon code counts referenced in the order by the given value.
225
	 *
226
	 * @param \Aimeos\MShop\Order\Item\Iface $orderItem Order item object
227
	 * @param int $how Positive or negative integer number for increasing or decreasing the coupon count
228
	 * @return \Aimeos\Controller\Common\Order\Iface Order controller for fluent interface
229
	 */
230
	protected function updateCoupons( \Aimeos\MShop\Order\Item\Iface $orderItem, int $how = +1 )
231
	{
232
		$context = $this->context();
233
		$manager = \Aimeos\MShop::create( $context, 'order/coupon' );
234
		$couponCodeManager = \Aimeos\MShop::create( $context, 'coupon/code' );
235
236
		$search = $manager->filter();
237
		$search->setConditions( $search->compare( '==', 'order.coupon.parentid', $orderItem->getId() ) );
238
239
		$start = 0;
240
241
		$couponCodeManager->begin();
242
243
		try
244
		{
245
			do
246
			{
247
				$items = $manager->search( $search );
248
249
				foreach( $items as $item ) {
250
					$couponCodeManager->decrease( $item->getCode(), $how * -1 );
251
				}
252
253
				$count = count( $items );
254
				$start += $count;
255
				$search->slice( $start );
256
			}
257
			while( $count >= $search->getLimit() );
258
259
			$couponCodeManager->commit();
260
		}
261
		catch( \Exception $e )
262
		{
263
			$couponCodeManager->rollback();
264
			throw $e;
265
		}
266
267
		return $this;
268
	}
269
270
271
	/**
272
	 * Increases or decreases the stock level or the coupon code count for referenced items of the given order.
273
	 *
274
	 * @param \Aimeos\MShop\Order\Item\Iface $orderItem Order item object
275
	 * @param string $type Constant from \Aimeos\MShop\Order\Item\Status\Base, e.g. STOCK_UPDATE or COUPON_UPDATE
276
	 * @param string $status New status value stored along with the order item
277
	 * @param int $value Number to increse or decrease the stock level or coupon code count
278
	 * @return \Aimeos\Controller\Common\Order\Iface Order controller for fluent interface
279
	 */
280
	protected function updateStatus( \Aimeos\MShop\Order\Item\Iface $orderItem, string $type, string $status, int $value )
281
	{
282
		$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\MShop\Order\Manag...te::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

282
		$statusItem = $this->getLastStatusItem( /** @scrutinizer ignore-type */ $orderItem->getId(), $type, $status );
Loading history...
283
284
		if( $statusItem && $statusItem->getValue() == $status ) {
285
			return;
286
		}
287
288
		if( $type == \Aimeos\MShop\Order\Item\Status\Base::STOCK_UPDATE ) {
289
			$this->updateStock( $orderItem, $value );
290
		} elseif( $type == \Aimeos\MShop\Order\Item\Status\Base::COUPON_UPDATE ) {
291
			$this->updateCoupons( $orderItem, $value );
292
		}
293
294
		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\MShop\Order\Manager\Update::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

294
		return $this->addStatusItem( /** @scrutinizer ignore-type */ $orderItem->getId(), $type, $status );
Loading history...
295
	}
296
297
298
	/**
299
	 * Increases or decreses the stock levels of the products referenced in the order by the given value.
300
	 *
301
	 * @param \Aimeos\MShop\Order\Item\Iface $orderItem Order item object
302
	 * @param int $how Positive or negative integer number for increasing or decreasing the stock levels
303
	 * @return \Aimeos\Controller\Common\Order\Iface Order controller for fluent interface
304
	 */
305
	protected function updateStock( \Aimeos\MShop\Order\Item\Iface $orderItem, int $how = +1 )
306
	{
307
		$context = $this->context();
308
		$stockManager = \Aimeos\MShop::create( $context, 'stock' );
309
		$manager = \Aimeos\MShop::create( $context, 'order/product' );
310
311
		$search = $manager->filter();
312
		$search->setConditions( $search->compare( '==', 'order.product.parentid', $orderItem->getId() ) );
313
314
		$start = 0;
315
316
		$stockManager->begin();
317
318
		try
319
		{
320
			do
321
			{
322
				$items = $manager->search( $search );
323
324
				foreach( $items as $item )
325
				{
326
					$stockManager->decrease( [$item->getProductId() => -1 * $how * $item->getQuantity()], $item->getStockType() );
327
328
					switch( $item->getType() ) {
329
						case 'default':
330
							$this->updateStockBundle( $item->getParentProductId(), $item->getStockType() ); break;
331
						case 'select':
332
							$this->updateStockSelection( $item->getParentProductId(), $item->getStockType() ); break;
333
					}
334
				}
335
336
				$count = count( $items );
337
				$start += $count;
338
				$search->slice( $start );
339
			}
340
			while( $count >= $search->getLimit() );
341
342
			$stockManager->commit();
343
		}
344
		catch( \Exception $e )
345
		{
346
			$stockManager->rollback();
347
			throw $e;
348
		}
349
350
		return $this;
351
	}
352
353
354
	/**
355
	 * Updates the stock levels of bundles for a specific type
356
	 *
357
	 * @param string $prodId Unique product ID
358
	 * @param string $stockType Unique stock type
359
	 * @return \Aimeos\Controller\Common\Order\Iface Order controller for fluent interface
360
	 */
361
	protected function updateStockBundle( string $prodId, string $stockType )
362
	{
363
		if( ( $bundleMap = $this->getBundleMap( $prodId ) ) === [] ) {
364
			return;
365
		}
366
367
368
		$bundleIds = $stock = [];
369
370
		foreach( $this->getStockItems( array_keys( $bundleMap ), $stockType ) as $stockItem )
371
		{
372
			if( isset( $bundleMap[$stockItem->getProductId()] ) && $stockItem->getStockLevel() !== null )
373
			{
374
				foreach( $bundleMap[$stockItem->getProductId()] as $bundleId )
375
				{
376
					if( isset( $stock[$bundleId] ) ) {
377
						$stock[$bundleId] = min( $stock[$bundleId], $stockItem->getStockLevel() );
378
					} else {
379
						$stock[$bundleId] = $stockItem->getStockLevel();
380
					}
381
382
					$bundleIds[$bundleId] = null;
383
				}
384
			}
385
		}
386
387
		if( empty( $stock ) ) {
388
			return;
389
		}
390
391
		$stockManager = \Aimeos\MShop::create( $this->context(), 'stock' );
392
393
		foreach( $this->getStockItems( array_keys( $bundleIds ), $stockType ) as $item )
394
		{
395
			if( isset( $stock[$item->getProductId()] ) )
396
			{
397
				$item->setStockLevel( $stock[$item->getProductId()] );
398
				$stockManager->save( $item );
399
			}
400
		}
401
402
		return $this;
403
	}
404
405
406
	/**
407
	 * Updates the stock levels of selection products for a specific type
408
	 *
409
	 * @param string $prodId Unique product ID
410
	 * @param string $stocktype Unique stock type
411
	 * @return \Aimeos\Controller\Common\Order\Iface Order controller for fluent interface
412
	 */
413
	protected function updateStockSelection( string $prodId, string $stocktype )
414
	{
415
		$stockManager = \Aimeos\MShop::create( $this->context(), 'stock' );
416
		$productManager = \Aimeos\MShop::create( $this->context(), 'product' );
417
418
		$productItem = $productManager->get( $prodId, ['product'] );
419
		$prodIds = $productItem->getRefItems( 'product', 'default', 'default' )->getId()->push( $productItem->getId() );
420
421
		$stockItems = $this->getStockItems( $prodIds, $stocktype )->col( null, 'stock.productid' );
422
		$selStockItem = $stockItems->pull( $prodId ) ?: $stockManager->create();
423
424
		$sum = $stockItems->getStockLevel()->reduce( function( $result, $value ) {
425
			return $result !== null && $value !== null ? $result + $value : null;
426
		}, 0 );
427
428
		$selStockItem->setProductId( $productItem->getId() )->setType( $stocktype )->setStockLevel( $sum );
429
		$stockManager->save( $selStockItem, false );
430
431
		return $this;
432
	}
433
}
434