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 ) |
|
|
|
|
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() |
|
|
|
|
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
|
|
|
|
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
.