Passed
Push — master ( d10a1a...f8298b )
by Aimeos
07:58
created

Base::checkQuantity()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 4
c 0
b 0
f 0
dl 0
loc 9
rs 10
cc 3
nc 2
nop 2
1
<?php
2
3
/**
4
 * @license LGPLv3, http://opensource.org/licenses/LGPL-3.0
5
 * @copyright Metaways Infosystems GmbH, 2012
6
 * @copyright Aimeos (aimeos.org), 2015-2020
7
 * @package Controller
8
 * @subpackage Frontend
9
 */
10
11
12
namespace Aimeos\Controller\Frontend\Basket;
13
14
15
/**
16
 * Base class for the basket frontend controller
17
 *
18
 * @package Controller
19
 * @subpackage Frontend
20
 */
21
abstract class Base extends \Aimeos\Controller\Frontend\Base implements Iface
22
{
23
	/**
24
	 * Calculates and returns the current price for the given order product and product prices.
25
	 *
26
	 * @param \Aimeos\MShop\Order\Item\Base\Product\Iface $orderProduct Ordered product item
27
	 * @param \Aimeos\Map $prices List of price items implementing \Aimeos\MShop\Price\Item\Iface
28
	 * @param float $quantity New product quantity
29
	 * @return \Aimeos\MShop\Price\Item\Iface Price item with calculated price
30
	 */
31
	protected function calcPrice( \Aimeos\MShop\Order\Item\Base\Product\Iface $orderProduct,
32
		\Aimeos\Map $prices, float $quantity ) : \Aimeos\MShop\Price\Item\Iface
33
	{
34
		$context = $this->getContext();
35
36
		$priceManager = \Aimeos\MShop::create( $context, 'price' );
37
		$price = $priceManager->getLowestPrice( $prices, $quantity );
38
39
		// customers can pay what they would like to pay
40
		if( ( $attr = $orderProduct->getAttributeItem( 'price', 'custom' ) ) !== null )
41
		{
42
			$amount = $attr->getValue();
43
44
			if( preg_match( '/^[0-9]*(\.[0-9]+)?$/', $amount ) !== 1 || ( (double) $amount ) < 0.01 )
0 ignored issues
show
Bug introduced by
It seems like $amount can also be of type array; however, parameter $subject of preg_match() 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

44
			if( preg_match( '/^[0-9]*(\.[0-9]+)?$/', /** @scrutinizer ignore-type */ $amount ) !== 1 || ( (double) $amount ) < 0.01 )
Loading history...
45
			{
46
				$msg = $context->getI18n()->dt( 'controller/frontend', 'Invalid price value "%1$s"' );
47
				throw new \Aimeos\Controller\Frontend\Basket\Exception( sprintf( $msg, $amount ) );
0 ignored issues
show
Bug introduced by
It seems like $amount can also be of type array; however, parameter $args of sprintf() 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

47
				throw new \Aimeos\Controller\Frontend\Basket\Exception( sprintf( $msg, /** @scrutinizer ignore-type */ $amount ) );
Loading history...
48
			}
49
50
			$price = $price->setValue( $amount );
51
		}
52
53
		$orderAttributes = $orderProduct->getAttributeItems();
54
		$attrItems = $this->getAttributeItems( $orderAttributes );
55
56
		// add prices of (optional) attributes
57
		foreach( $orderAttributes as $orderAttrItem )
58
		{
59
			if( ( $attrItem = $attrItems->get( $orderAttrItem->getAttributeId() ) ) !== null
60
				&& !( $prices = $attrItem->getRefItems( 'price', 'default', 'default' ) )->isEmpty()
61
			) {
62
				$attrPrice = $priceManager->getLowestPrice( $prices, $orderAttrItem->getQuantity() );
63
				$price = $price->addItem( $attrPrice, $orderAttrItem->getQuantity() );
64
			}
65
		}
66
67
		// remove product rebate of original price in favor to rebates granted for the order
68
		return $price->setRebate( '0.00' );
69
	}
70
71
72
	/**
73
	 * Returns the allowed quantity for the given product
74
	 *
75
	 * @param \Aimeos\MShop\Product\Item\Iface $product Product item including referenced items
76
	 * @param float $quantity New product quantity
77
	 * @return float Updated quantity value
78
	 */
79
	protected function checkQuantity( \Aimeos\MShop\Product\Item\Iface $product, float $quantity ) : float
80
	{
81
		$step = $product->getConfigValue( 'quantity-step', 1 ) ?: 1;
82
83
		if( fmod( $quantity, $step ) >= 0.0005 ) {
84
			return ceil( $quantity / $step ) * $step;
85
		}
86
87
		return $quantity;
88
	}
89
90
91
	/**
92
	 * Checks if the attribute IDs are really associated to the product
93
	 *
94
	 * @param \Aimeos\MShop\Product\Item\Iface $product Product item with referenced items
95
	 * @param string $domain Domain the references must be of
96
	 * @param array $refMap Associative list of list type codes as keys and lists of reference IDs as values
97
	 * @throws \Aimeos\Controller\Frontend\Basket\Exception If one or more of the IDs are not associated
98
	 */
99
	protected function checkAttributes( array $products, string $listType, array $refIds )
100
	{
101
		$attrIds = map();
102
103
		foreach( $products as $product ) {
104
			$attrIds->merge( $product->getRefItems( 'attribute', null, $listType )->keys() );
105
		}
106
107
		if( $attrIds->intersect( $refIds )->count() !== count( $refIds ) )
108
		{
109
			$i18n = $this->getContext()->getI18n();
110
			$prodIds = map( $products )->getId()->join( ', ' );
111
			$msg = $i18n->dt( 'controller/frontend', 'Invalid "%1$s" references for product with ID %2$s' );
112
			throw new \Aimeos\Controller\Frontend\Basket\Exception( sprintf( $msg, 'attribute', $prodIds ) );
113
		}
114
	}
115
116
117
	/**
118
	 * Checks for a locale mismatch and migrates the products to the new basket if necessary.
119
	 *
120
	 * @param \Aimeos\MShop\Locale\Item\Iface $locale Locale object from current basket
121
	 * @param string $type Basket type
122
	 */
123
	protected function checkLocale( \Aimeos\MShop\Locale\Item\Iface $locale, string $type )
124
	{
125
		$errors = [];
126
		$context = $this->getContext();
127
		$session = $context->getSession();
128
129
		$localeStr = $session->get( 'aimeos/basket/locale' );
130
		$localeKey = $locale->getSiteItem()->getCode() . '|' . $locale->getLanguageId() . '|' . $locale->getCurrencyId();
131
132
		if( $localeStr !== null && $localeStr !== $localeKey )
133
		{
134
			$locParts = explode( '|', $localeStr );
135
			$locSite = ( isset( $locParts[0] ) ? $locParts[0] : '' );
136
			$locLanguage = ( isset( $locParts[1] ) ? $locParts[1] : '' );
137
			$locCurrency = ( isset( $locParts[2] ) ? $locParts[2] : '' );
138
139
			$localeManager = \Aimeos\MShop::create( $context, 'locale' );
140
			$locale = $localeManager->bootstrap( $locSite, $locLanguage, $locCurrency, false );
141
142
			$context = clone $context;
143
			$context->setLocale( $locale );
144
145
			$manager = \Aimeos\MShop\Order\Manager\Factory::create( $context )->getSubManager( 'base' );
146
			$basket = $manager->getSession( $type )->off();
147
148
			$this->copyAddresses( $basket, $errors, $localeKey );
149
			$this->copyServices( $basket, $errors );
150
			$this->copyProducts( $basket, $errors, $localeKey );
151
			$this->copyCoupons( $basket, $errors, $localeKey );
152
153
			$this->getObject()->get()->setCustomerId( $basket->getCustomerId() )
154
				->setCustomerReference( $basket->getCustomerReference() )
155
				->setComment( $basket->getComment() );
156
157
			$manager->setSession( $basket, $type );
158
		}
159
160
		$session->set( 'aimeos/basket/locale', $localeKey );
161
	}
162
163
164
	/**
165
	 * Migrates the addresses from the old basket to the current one.
166
	 *
167
	 * @param \Aimeos\MShop\Order\Item\Base\Iface $basket Basket object
168
	 * @param array $errors Associative list of previous errors
169
	 * @param string $localeKey Unique identifier of the site, language and currency
170
	 * @return array Associative list of errors occured
171
	 */
172
	protected function copyAddresses( \Aimeos\MShop\Order\Item\Base\Iface $basket, array $errors, string $localeKey ) : array
173
	{
174
		foreach( $basket->getAddresses() as $type => $items )
175
		{
176
			foreach( $items as $pos => $item )
177
			{
178
				try
179
				{
180
					$this->getObject()->get()->addAddress( $item, $type, $pos );
181
				}
182
				catch( \Exception $e )
183
				{
184
					$logger = $this->getContext()->getLogger();
185
					$errors['address'][$type] = $e->getMessage();
186
187
					$str = 'Error migrating address with type "%1$s" in basket to locale "%2$s": %3$s';
188
					$logger->log( sprintf( $str, $type, $localeKey, $e->getMessage() ), \Aimeos\MW\Logger\Base::INFO );
189
				}
190
			}
191
192
			$basket->deleteAddress( $type );
193
		}
194
195
		return $errors;
196
	}
197
198
199
	/**
200
	 * Migrates the coupons from the old basket to the current one.
201
	 *
202
	 * @param \Aimeos\MShop\Order\Item\Base\Iface $basket Basket object
203
	 * @param array $errors Associative list of previous errors
204
	 * @param string $localeKey Unique identifier of the site, language and currency
205
	 * @return array Associative list of errors occured
206
	 */
207
	protected function copyCoupons( \Aimeos\MShop\Order\Item\Base\Iface $basket, array $errors, string $localeKey ) : array
208
	{
209
		foreach( $basket->getCoupons() as $code => $list )
210
		{
211
			try
212
			{
213
				$this->getObject()->addCoupon( $code );
214
				$basket->deleteCoupon( $code, true );
0 ignored issues
show
Unused Code introduced by
The call to Aimeos\MShop\Order\Item\Base\Iface::deleteCoupon() has too many arguments starting with true. ( Ignorable by Annotation )

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

214
				$basket->/** @scrutinizer ignore-call */ 
215
             deleteCoupon( $code, true );

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
215
			}
216
			catch( \Exception $e )
217
			{
218
				$logger = $this->getContext()->getLogger();
219
				$errors['coupon'][$code] = $e->getMessage();
220
221
				$str = 'Error migrating coupon with code "%1$s" in basket to locale "%2$s": %3$s';
222
				$logger->log( sprintf( $str, $code, $localeKey, $e->getMessage() ), \Aimeos\MW\Logger\Base::INFO );
223
			}
224
		}
225
226
		return $errors;
227
	}
228
229
230
	/**
231
	 * Migrates the products from the old basket to the current one.
232
	 *
233
	 * @param \Aimeos\MShop\Order\Item\Base\Iface $basket Basket object
234
	 * @param array $errors Associative list of previous errors
235
	 * @param string $localeKey Unique identifier of the site, language and currency
236
	 * @return array Associative list of errors occured
237
	 */
238
	protected function copyProducts( \Aimeos\MShop\Order\Item\Base\Iface $basket, array $errors, string $localeKey ) : array
239
	{
240
		$domains = ['attribute', 'media', 'price', 'product', 'text'];
241
		$manager = \Aimeos\MShop::create( $this->getContext(), 'product' );
242
243
		foreach( $basket->getProducts() as $pos => $product )
244
		{
245
			if( $product->getFlags() & \Aimeos\MShop\Order\Item\Base\Product\Base::FLAG_IMMUTABLE ) {
246
				continue;
247
			}
248
249
			try
250
			{
251
				$variantIds = $configIds = $customIds = [];
252
253
				foreach( $product->getAttributeItems() as $attrItem )
254
				{
255
					switch( $attrItem->getType() )
256
					{
257
						case 'variant': $variantIds[] = $attrItem->getAttributeId(); break;
258
						case 'config': $configIds[$attrItem->getAttributeId()] = $attrItem->getQuantity(); break;
259
						case 'custom': $customIds[$attrItem->getAttributeId()] = $attrItem->getValue(); break;
260
					}
261
				}
262
263
				$item = $manager->getItem( $product->getProductId(), $domains );
264
265
				$this->getObject()->addProduct(
266
					$item, $product->getQuantity(), $variantIds, $configIds, $customIds,
267
					$product->getStockType(), $product->getSupplierCode()
268
				);
269
270
				$basket->deleteProduct( $pos );
271
			}
272
			catch( \Exception $e )
273
			{
274
				$code = $product->getProductCode();
275
				$logger = $this->getContext()->getLogger();
276
				$errors['product'][$pos] = $e->getMessage();
277
278
				$str = 'Error migrating product with code "%1$s" in basket to locale "%2$s": %3$s';
279
				$logger->log( sprintf( $str, $code, $localeKey, $e->getMessage() ), \Aimeos\MW\Logger\Base::INFO );
280
			}
281
		}
282
283
		return $errors;
284
	}
285
286
287
	/**
288
	 * Migrates the services from the old basket to the current one.
289
	 *
290
	 * @param \Aimeos\MShop\Order\Item\Base\Iface $basket Basket object
291
	 * @param array $errors Associative list of previous errors
292
	 * @return array Associative list of errors occured
293
	 */
294
	protected function copyServices( \Aimeos\MShop\Order\Item\Base\Iface $basket, array $errors ) : array
295
	{
296
		$manager = \Aimeos\MShop::create( $this->getContext(), 'service' );
297
298
		foreach( $basket->getServices() as $type => $list )
299
		{
300
			foreach( $list as $item )
301
			{
302
				try
303
				{
304
					$attributes = [];
305
306
					foreach( $item->getAttributeItems() as $attrItem ) {
307
						$attributes[$attrItem->getCode()] = $attrItem->getValue();
308
					}
309
310
					$service = $manager->getItem( $item->getServiceId(), ['media', 'price', 'text'] );
311
					$this->getObject()->addService( $service, $attributes );
312
					$basket->deleteService( $type );
313
				}
314
				catch( \Exception $e ) {; } // Don't notify the user as appropriate services can be added automatically
315
			}
316
		}
317
318
		return $errors;
319
	}
320
321
322
	/**
323
	 * Creates the subscription entries for the ordered products with interval attributes
324
	 *
325
	 * @param \Aimeos\MShop\Order\Item\Base\Iface $basket Basket object
326
	 */
327
	protected function createSubscriptions( \Aimeos\MShop\Order\Item\Base\Iface $basket )
328
	{
329
		$types = ['config', 'custom', 'hidden', 'variant'];
330
		$manager = \Aimeos\MShop::create( $this->getContext(), 'subscription' );
331
332
		foreach( $basket->getProducts() as $orderProduct )
333
		{
334
			if( ( $interval = $orderProduct->getAttribute( 'interval', $types ) ) !== null )
335
			{
336
				$interval = is_array( $interval ) ? reset( $interval ) : $interval;
337
338
				$item = $manager->createItem()->setInterval( $interval )
339
					->setProductId( $orderProduct->getProductId() )
340
					->setOrderProductId( $orderProduct->getId() )
341
					->setOrderBaseId( $basket->getId() );
342
343
				if( ( $end = $orderProduct->getAttribute( 'intervalend', $types ) ) !== null ) {
344
					$item = $item->setDateEnd( $end );
345
				}
346
347
				$manager->saveItem( $item, false );
0 ignored issues
show
Bug introduced by
The method saveItem() does not exist on Aimeos\MShop\Common\Manager\Iface. Did you maybe mean saveItems()? ( Ignorable by Annotation )

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

347
				$manager->/** @scrutinizer ignore-call */ 
348
              saveItem( $item, false );

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...
348
			}
349
		}
350
	}
351
352
353
	/**
354
	 * Returns the attribute items for the given attribute IDs.
355
	 *
356
	 * @param array $attributeIds List of attribute IDs
357
	 * @param string[] $domains Names of the domain items that should be fetched too
358
	 * @return \Aimeos\Map List of items implementing \Aimeos\MShop\Attribute\Item\Iface
359
	 * @throws \Aimeos\Controller\Frontend\Basket\Exception If the actual attribute number doesn't match the expected one
360
	 */
361
	protected function getAttributes( array $attributeIds, array $domains = ['text'] ) : \Aimeos\Map
362
	{
363
		if( empty( $attributeIds ) ) {
364
			return map();
365
		}
366
367
		$attributeManager = \Aimeos\MShop::create( $this->getContext(), 'attribute' );
368
369
		$search = $attributeManager->createSearch( true );
370
		$expr = array(
371
			$search->compare( '==', 'attribute.id', $attributeIds ),
372
			$search->getConditions(),
373
		);
374
		$search->setConditions( $search->combine( '&&', $expr ) );
375
		$search->setSlice( 0, count( $attributeIds ) );
376
377
		$attrItems = $attributeManager->searchItems( $search, $domains );
378
379
		if( $attrItems->count() !== count( $attributeIds ) )
380
		{
381
			$i18n = $this->getContext()->getI18n();
382
			$expected = implode( ',', $attributeIds );
383
			$actual = $attrItems->keys()->join( ',' );
384
			$msg = $i18n->dt( 'controller/frontend', 'Available attribute IDs "%1$s" do not match the given attribute IDs "%2$s"' );
385
386
			throw new \Aimeos\Controller\Frontend\Basket\Exception( sprintf( $msg, $actual, $expected ) );
387
		}
388
389
		return $attrItems;
390
	}
391
392
393
	/**
394
	 * Returns the attribute items using the given order attribute items.
395
	 *
396
	 * @param \Aimeos\Map $orderAttributes List of items implementing \Aimeos\MShop\Order\Item\Base\Product\Attribute\Iface
397
	 * @return \Aimeos\Map List of attribute IDs as key and attribute items implementing \Aimeos\MShop\Attribute\Item\Iface
398
	 */
399
	protected function getAttributeItems( \Aimeos\Map $orderAttributes ) : \Aimeos\Map
400
	{
401
		if( $orderAttributes->isEmpty() ) {
402
			return map();
403
		}
404
405
		$attributeManager = \Aimeos\MShop::create( $this->getContext(), 'attribute' );
406
		$search = $attributeManager->createSearch( true );
407
		$expr = [];
408
409
		foreach( $orderAttributes as $item )
410
		{
411
			$tmp = array(
412
				$search->compare( '==', 'attribute.domain', 'product' ),
413
				$search->compare( '==', 'attribute.code', $item->getValue() ),
414
				$search->compare( '==', 'attribute.type', $item->getCode() ),
415
				$search->compare( '>', 'attribute.status', 0 ),
416
				$search->getConditions(),
417
			);
418
			$expr[] = $search->combine( '&&', $tmp );
419
		}
420
421
		$search->setConditions( $search->combine( '||', $expr ) );
422
		return $attributeManager->searchItems( $search, array( 'price' ) );
423
	}
424
425
426
	/**
427
	 * Returns the order product attribute items for the given IDs and values
428
	 *
429
	 * @param string $type Attribute type code
430
	 * @param array $ids List of attributes IDs of the given type
431
	 * @param array $values Associative list of attribute IDs as keys and their codes as values
432
	 * @param array $quantities Associative list of attribute IDs as keys and their quantities as values
433
	 * @return array List of items implementing \Aimeos\MShop\Order\Item\Product\Attribute\Iface
434
	 */
435
	protected function getOrderProductAttributes( string $type, array $ids, array $values = [], array $quantities = [] )
436
	{
437
		$list = [];
438
439
		if( !empty( $ids ) )
440
		{
441
			$manager = \Aimeos\MShop::create( $this->getContext(), 'order/base/product/attribute' );
442
443
			foreach( $this->getAttributes( $ids ) as $id => $attrItem )
444
			{
445
				$list[] = $manager->createItem()->copyFrom( $attrItem )->setType( $type )
446
					->setValue( isset( $values[$id] ) ? $values[$id] : $attrItem->getCode() )
447
					->setQuantity( isset( $quantities[$id] ) ? $quantities[$id] : 1 );
448
			}
449
		}
450
451
		return $list;
452
	}
453
}
454