Passed
Push — master ( b2da90...4d83d5 )
by Aimeos
06:05
created

ProductPrice::getConfigBE()   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, http://opensource.org/licenses/LGPL-3.0
5
 * @copyright Metaways Infosystems GmbH, 2012
6
 * @copyright Aimeos (aimeos.org), 2015-2018
7
 * @package MShop
8
 * @subpackage Plugin
9
 */
10
11
12
namespace Aimeos\MShop\Plugin\Provider\Order;
13
14
15
/**
16
 * Checks the products in a basket for changed prices
17
 *
18
 * Notifies the customers if a price of a product in the basket has changed in
19
 * the meantime. This plugin can handle the change from net to gross prices and
20
 * backwards if prices are recalculated for B2B or B2C customers. In these cases
21
 * the customer won't be notified.
22
 *
23
 * The following option is available:
24
 * - ignore-modified: Set to true if all basket items with modified prices (e.g. by
25
 *   another plugin) should be excluded from the check. Uses the isModified() method
26
 *   of the item's price object.
27
 *
28
 * To trace the execution and interaction of the plugins, set the log level to DEBUG:
29
 *	madmin/log/manager/standard/loglevel = 7
30
 *
31
 * @package MShop
32
 * @subpackage Plugin
33
 */
34
class ProductPrice
35
	extends \Aimeos\MShop\Plugin\Provider\Factory\Base
36
	implements \Aimeos\MShop\Plugin\Provider\Iface, \Aimeos\MShop\Plugin\Provider\Factory\Iface
37
{
38
	private $beConfig = array(
39
		'ignore-modified' => array(
40
			'code' => 'ignore-modified',
41
			'internalcode' => 'ignore-modified',
42
			'label' => 'Ignore order items with a modified price (e.g. by another plugin)',
43
			'type' => 'boolean',
44
			'internaltype' => 'boolean',
45
			'default' => '0',
46
			'required' => false,
47
		),
48
	);
49
50
51
	/**
52
	 * Checks the backend configuration attributes for validity.
53
	 *
54
	 * @param array $attributes Attributes added by the shop owner in the administraton interface
55
	 * @return array An array with the attribute keys as key and an error message as values for all attributes that are
56
	 * 	known by the provider but aren't valid
57
	 */
58
	public function checkConfigBE( array $attributes )
0 ignored issues
show
Coding Style introduced by
This method is not in camel caps format.

This check looks for method names that are not written in camelCase.

In camelCase names are written without any punctuation, the start of each new word being marked by a capital letter. Thus the name database connection seeker becomes databaseConnectionSeeker.

Loading history...
59
	{
60
		$errors = parent::checkConfigBE( $attributes );
61
62
		return array_merge( $errors, $this->checkConfig( $this->beConfig, $attributes ) );
63
	}
64
65
66
	/**
67
	 * Returns the configuration attribute definitions of the provider to generate a list of available fields and
68
	 * rules for the value of each field in the administration interface.
69
	 *
70
	 * @return array List of attribute definitions implementing \Aimeos\MW\Common\Critera\Attribute\Iface
71
	 */
72
	public function getConfigBE()
0 ignored issues
show
Coding Style introduced by
This method is not in camel caps format.

This check looks for method names that are not written in camelCase.

In camelCase names are written without any punctuation, the start of each new word being marked by a capital letter. Thus the name database connection seeker becomes databaseConnectionSeeker.

Loading history...
73
	{
74
		return $this->getConfigItems( $this->beConfig );
75
	}
76
77
78
	/**
79
	 * Subscribes itself to a publisher
80
	 *
81
	 * @param \Aimeos\MW\Observer\Publisher\Iface $p Object implementing publisher interface
82
	 * @return \Aimeos\MShop\Plugin\Provider\Iface Plugin object for method chaining
83
	 */
84
	public function register( \Aimeos\MW\Observer\Publisher\Iface $p )
85
	{
86
		$p->attach( $this->getObject(), 'check.after' );
87
		return $this;
88
	}
89
90
91
	/**
92
	 * Receives a notification from a publisher object
93
	 *
94
	 * @param \Aimeos\MW\Observer\Publisher\Iface $order Shop basket instance implementing publisher interface
95
	 * @param string $action Name of the action to listen for
96
	 * @param mixed $value Object or value changed in publisher
97
	 * @return mixed Modified value parameter
98
	 * @throws \Aimeos\MShop\Plugin\Provider\Exception if checks fail
99
	 */
100
	public function update( \Aimeos\MW\Observer\Publisher\Iface $order, $action, $value = null )
101
	{
102
		if( ( $value & \Aimeos\MShop\Order\Item\Base\Base::PARTS_PRODUCT ) === 0 ) {
103
			return $value;
104
		}
105
106
		\Aimeos\MW\Common\Base::checkClass( \Aimeos\MShop\Order\Item\Base\Iface::class, $order );
107
108
		$attrIds = $prodCodes = $changedProducts = [];
109
		$orderProducts = $order->getProducts();
110
111
		foreach( $orderProducts as $pos => $item )
112
		{
113
			if( $item->getFlags() & \Aimeos\MShop\Order\Item\Base\Product\Base::FLAG_IMMUTABLE ) {
114
				unset( $orderProducts[$pos] );
115
			}
116
			
117
			if( $this->getConfigValue( 'ignore-modified' ) == true && $item->getPrice()->isModified() ) {
118
				unset( $orderProducts[$pos] );
119
			}
120
121
			$prodCodes[] = $item->getProductCode();
122
123
			foreach( $item->getAttributeItems() as $ordAttrItem )
124
			{
125
				if( ( $id = $ordAttrItem->getAttributeId() ) != '' ) {
126
					$attrIds[] = $id;
127
				}
128
			}
129
		}
130
131
132
		$attributes = $this->getAttributeItems( array_unique( $attrIds ) );
133
		$prodMap = $this->getProductItems( $prodCodes );
134
135
136
		foreach( $orderProducts as $pos => $orderProduct )
137
		{
138
			if( !isset( $prodMap[$orderProduct->getProductCode()] ) ) {
139
				continue; // Product isn't available or excluded
140
			}
141
142
			// fetch prices of articles/sub-products
143
			$refPrices = $prodMap[$orderProduct->getProductCode()]->getRefItems( 'price', 'default', 'default' );
144
145
			$orderPosPrice = $orderProduct->getPrice();
146
			$price = $this->getPrice( $orderProduct, $refPrices, $attributes, $pos );
147
148
			if( $orderPosPrice->getTaxFlag() === $price->getTaxFlag() && $orderPosPrice->compare( $price ) === false )
149
			{
150
				$order->addProduct( $orderProduct->setPrice( $price ), $pos );
151
				$changedProducts[$pos] = 'price.changed';
152
			}
153
		}
154
155
		if( count( $changedProducts ) > 0 )
156
		{
157
			$code = array( 'product' => $changedProducts );
158
			$msg = $this->getContext()->getI18n()->dt( 'mshop', 'Please have a look at the prices of the products in your basket' );
159
			throw new \Aimeos\MShop\Plugin\Provider\Exception( $msg, -1, null, $code );
160
		}
161
162
		return $value;
163
	}
164
165
166
	/**
167
	 * Returns the attribute items for the given IDs.
168
	 *
169
	 * @param array $list List of attribute IDs
170
	 * @return \Aimeos\MShop\Attribute\Item\Iface[] List of attribute items
171
	 */
172
	protected function getAttributeItems( array $list )
173
	{
174
		if( $list !== [] )
175
		{
176
			$attrManager = \Aimeos\MShop::create( $this->getContext(), 'attribute' );
177
178
			$search = $attrManager->createSearch( true );
179
			$expr = [$search->compare( '==', 'attribute.id', $list ), $search->getConditions()];
180
			$search->setConditions( $search->combine( '&&', $expr ) );
181
182
			$list = $attrManager->searchItems( $search, ['price'] );
183
		}
184
185
		return $list;
186
	}
187
188
189
	/**
190
	 * Returns the product items for the given product codes.
191
	 *
192
	 * @param string[] $prodCodes Product codes
193
	 * @return \Aimeos\MShop\Product\Item\Iface[] Associative list of codes as keys and product items as values
194
	 */
195
	protected function getProductItems( array $prodCodes )
196
	{
197
		if( empty( $prodCodes ) ) {
198
			return [];
199
		}
200
201
		$attrManager = \Aimeos\MShop::create( $this->getContext(), 'attribute' );
202
		$productManager = \Aimeos\MShop::create( $this->getContext(), 'product' );
203
204
		$attrId = $attrManager->findItem( 'custom', [], 'product', 'price' )->getId();
205
206
		$search = $productManager->createSearch( true )->setSlice( 0, count( $prodCodes ) );
207
		$func = $search->createFunction( 'product:has', ['attribute', 'custom', $attrId] );
208
		$expr = array(
209
			$search->compare( '==', 'product.code', $prodCodes ),
210
			$search->compare( '==', $func, null ),
211
			$search->getConditions(),
212
		);
213
		$search->setConditions( $search->combine( '&&', $expr ) );
214
215
		$map = [];
216
217
		foreach( $productManager->searchItems( $search, ['price'] ) as $item ) {
218
			$map[$item->getCode()] = $item;
219
		}
220
221
		return $map;
222
	}
223
224
225
	/**
226
	 * Returns the actual price for the given order product.
227
	 *
228
	 * @param \Aimeos\MShop\Order\Item\Base\Product\Iface $orderProduct Ordered product
229
	 * @param \Aimeos\MShop\Price\Item\Iface[] $refPrices Prices associated to the original product
230
	 * @param \Aimeos\MShop\Attribute\Item\Iface[] $attributes Attribute items with prices
231
	 * @param integer $pos Position of the product in the basket
232
	 * @return \Aimeos\MShop\Price\Item\Iface Price item including the calculated price
233
	 */
234
	private function getPrice( \Aimeos\MShop\Order\Item\Base\Product\Iface $orderProduct, array $refPrices, array $attributes, $pos )
235
	{
236
		$context = $this->getContext();
237
238
		// fetch prices of selection/parent products
239
		if( empty( $refPrices ) )
240
		{
241
			$productManager = \Aimeos\MShop::create( $context, 'product' );
242
			$product = $productManager->getItem( $orderProduct->getProductId(), array( 'price' ) );
243
			$refPrices = $product->getRefItems( 'price', 'default', 'default' );
244
		}
245
246
		if( empty( $refPrices ) )
247
		{
248
			$pid = $orderProduct->getProductId();
249
			$pcode = $orderProduct->getProductCode();
250
			$codes = array( 'product' => array( $pos => 'product.price' ) );
251
			$msg = $this->getContext()->getI18n()->dt( 'mshop', 'No price for product ID "%1$s" or product code "%2$s" available' );
252
253
			throw new \Aimeos\MShop\Plugin\Provider\Exception( sprintf( $msg, $pid, $pcode ), -1, null, $codes );
254
		}
255
256
		$priceManager = \Aimeos\MShop::create( $context, 'price' );
257
		$price = clone $priceManager->getLowestPrice( $refPrices, $orderProduct->getQuantity() );
258
259
		// add prices of product attributes to compute the end price for comparison
260
		foreach( $orderProduct->getAttributeItems() as $orderAttribute )
261
		{
262
			$attrPrices = [];
263
			$attrId = $orderAttribute->getAttributeId();
264
265
			if( isset( $attributes[$attrId] ) ) {
266
				$attrPrices = $attributes[$attrId]->getRefItems( 'price', 'default', 'default' );
267
			}
268
269
			if( !empty( $attrPrices ) )
270
			{
271
				$lowPrice = $priceManager->getLowestPrice( $attrPrices, $orderAttribute->getQuantity() );
272
				$price = $price->addItem( $lowPrice, $orderAttribute->getQuantity() );
273
			}
274
		}
275
276
		// reset product rebates like in the basket controller
277
		return $price->setRebate( '0.00' );
278
	}
279
}
280