• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

aimeos / aimeos-core / f04e0d11-13e0-4c5a-b67d-4a9523686369

28 Mar 2025 03:03PM UTC coverage: 92.6% (+0.06%) from 92.539%
f04e0d11-13e0-4c5a-b67d-4a9523686369

push

circleci

aimeos
Avoid recalculating order price if order item is modified

18 of 18 new or added lines in 2 files covered. (100.0%)

5 existing lines in 1 file now uncovered.

9635 of 10405 relevant lines covered (92.6%)

80.46 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

94.35
/src/MShop/Order/Item/Base.php
1
<?php
2

3
/**
4
 * @license LGPLv3, https://opensource.org/licenses/LGPL-3.0
5
 * @copyright Aimeos (aimeos.org), 2015-2025
6
 * @package MShop
7
 * @subpackage Order
8
 */
9

10

11
namespace Aimeos\MShop\Order\Item;
12

13

14
/**
15
 * Base order item class with common constants and methods.
16
 *
17
 * @package MShop
18
 * @subpackage Order
19
 */
20
abstract class Base
21
        extends \Aimeos\MShop\Common\Item\Base
22
        implements \Aimeos\MShop\Order\Item\Iface, \Aimeos\Macro\Iface, \ArrayAccess, \JsonSerializable
23
{
24
        use \Aimeos\Macro\Macroable;
25
        use Publisher;
26

27

28
        /**
29
         * Unfinished delivery.
30
         * This is the default status after creating an order and this status
31
         * should be also used as long as technical errors occurs.
32
         */
33
        const STAT_UNFINISHED = -1;
34

35
        /**
36
         * Delivery was deleted.
37
         * The delivery of the order was deleted manually.
38
         */
39
        const STAT_DELETED = 0;
40

41
        /**
42
         * Delivery is pending.
43
         * The order is not yet in the fulfillment process until further actions
44
         * are taken.
45
         */
46
        const STAT_PENDING = 1;
47

48
        /**
49
         * Fulfillment in progress.
50
         * The delivery of the order is in the (internal) fulfillment process and
51
         * will be ready soon.
52
         */
53
        const STAT_PROGRESS = 2;
54

55
        /**
56
         * Parcel is dispatched.
57
         * The parcel was given to the logistic partner for delivery to the
58
         * customer.
59
         */
60
        const STAT_DISPATCHED = 3;
61

62
        /**
63
         * Parcel was delivered.
64
         * The logistic partner delivered the parcel and the customer received it.
65
         */
66
        const STAT_DELIVERED = 4;
67

68
        /**
69
         * Parcel is lost.
70
         * The parcel is lost during delivery by the logistic partner and haven't
71
         * reached the customer nor it's returned to the merchant.
72
         */
73
        const STAT_LOST = 5;
74

75
        /**
76
         * Parcel was refused.
77
         * The delivery of the parcel failed because the customer has refused to
78
         * accept it or the address was invalid.
79
         */
80
        const STAT_REFUSED = 6;
81

82
        /**
83
         * Parcel was returned.
84
         * The parcel was sent back by the customer.
85
         */
86
        const STAT_RETURNED = 7;
87

88

89
        /**
90
         * Unfinished payment.
91
         * This is the default status after creating an order and this status
92
         * should be also used as long as technical errors occurs.
93
         */
94
        const PAY_UNFINISHED = -1;
95

96
        /**
97
         * Payment was deleted.
98
         * The payment for the order was deleted manually.
99
         */
100
        const PAY_DELETED = 0;
101

102
        /**
103
         * Payment was canceled.
104
         * The customer canceled the payment process.
105
         */
106
        const PAY_CANCELED = 1;
107

108
        /**
109
         * Payment was refused.
110
         * The customer didn't enter valid payment details.
111
         */
112
        const PAY_REFUSED = 2;
113

114
        /**
115
         * Payment was refund.
116
         * The payment was OK but refund and the customer got his money back.
117
         */
118
        const PAY_REFUND = 3;
119

120
        /**
121
         * Payment is pending.
122
         * The payment is not yet done until further actions are taken.
123
         */
124
        const PAY_PENDING = 4;
125

126
        /**
127
         * Payment is authorized.
128
         * The customer authorized the merchant to invoice the amount but the money
129
         * is not yet received. This is used for all post-paid orders.
130
         */
131
        const PAY_AUTHORIZED = 5;
132

133
        /**
134
         * Payment is received.
135
         * The merchant received the money from the customer.
136
         */
137
        const PAY_RECEIVED = 6;
138

139
        /**
140
         * Payment is transferred.
141
         * The vendor received the money from the platform.
142
         */
143
        const PAY_TRANSFERRED = 7;
144

145

146
        // protected is a workaround for serialize problem
147
        protected ?\Aimeos\MShop\Customer\Item\Iface $customer;
148
        protected \Aimeos\MShop\Locale\Item\Iface $locale;
149
        protected \Aimeos\MShop\Price\Item\Iface $price;
150
        protected array $coupons = [];
151
        protected array $products = [];
152
        protected array $services = [];
153
        protected array $statuses = [];
154
        protected array $addresses = [];
155

156

157
        /**
158
         * Initializes the order object
159
         *
160
         * @param string $prefix Prefix for the keys in the associative array
161
         * @param array $values Associative list of key/value pairs containing, e.g. the order or user ID
162
         */
163
        public function __construct( string $prefix, array $values = [] )
164
        {
165
                $this->customer = $values['.customer'] ?? null;
547✔
166
                $this->locale = $values['.locale'];
547✔
167
                $this->price = $values['.price'];
547✔
168

169
                $products = $values['.products'] ?? [];
547✔
170

171
                foreach( $values['.coupons'] ?? [] as $coupon )
547✔
172
                {
173
                        if( !isset( $this->coupons[$coupon->getCode()] ) ) {
6✔
174
                                $this->coupons[$coupon->getCode()] = [];
6✔
175
                        }
176

177
                        if( isset( $products[$coupon->getProductId()] ) ) {
6✔
178
                                $this->coupons[$coupon->getCode()][] = $products[$coupon->getProductId()];
6✔
179
                        }
180
                }
181

182
                foreach( $values['.products'] ?? [] as $product ) {
547✔
183
                        $this->products[$product->getPosition()] = $product;
38✔
184
                }
185

186
                foreach( $values['.addresses'] ?? [] as $address ) {
547✔
187
                        $this->addresses[$address->getType()][] = $address;
32✔
188
                }
189

190
                foreach( $values['.services'] ?? [] as $service ) {
547✔
191
                        $this->services[$service->getType()][] = $service;
33✔
192
                }
193

194
                foreach( $values['.statuses'] ?? [] as $status ) {
547✔
195
                        $this->statuses[$status->getType()][$status->getValue()] = $status;
1✔
196
                }
197

198
                unset( $values['.customer'], $values['.locale'], $values['.price'], $values['.statuses'] );
547✔
199
                unset( $values['.products'], $values['.coupons'], $values['.addresses'], $values['.services'] );
547✔
200

201
                parent::__construct( $prefix, $values );
547✔
202
        }
203

204

205
        /**
206
         * Clones internal objects of the order item.
207
         */
208
        public function __clone()
209
        {
210
                parent::__clone();
4✔
211

212
                $this->price = clone $this->price;
4✔
213
                $this->locale = clone $this->locale;
4✔
214
        }
215

216

217
        /**
218
         * Specifies the data which should be serialized to JSON by json_encode().
219
         *
220
         * @return array<string,mixed> Data to serialize to JSON
221
         */
222
        #[\ReturnTypeWillChange]
223
        public function jsonSerialize()
224
        {
225
                return parent::jsonSerialize() + [
1✔
226
                        'addresses' => $this->addresses,
1✔
227
                        'products' => $this->products,
1✔
228
                        'services' => $this->services,
1✔
229
                        'coupons' => $this->coupons,
1✔
230
                        'customer' => $this->customer,
1✔
231
                        'locale' => $this->locale,
1✔
232
                        'price' => $this->price,
1✔
233
                ];
1✔
234
        }
235

236

237
        /**
238
         * Prepares the object for serialization.
239
         *
240
         * @return array List of properties that should be serialized
241
         */
242
        public function __sleep() : array
243
        {
244
                /*
245
                 * Workaround because database connections can't be serialized
246
                 * Listeners will be reattached on wakeup by the order base manager
247
                 */
248
                $this->off();
3✔
249

250
                return array_keys( get_object_vars( $this ) );
3✔
251
        }
252

253

254
        /**
255
         * Returns the ID of the items
256
         *
257
         * @return string ID of the item or null
258
         */
259
        public function __toString() : string
260
        {
261
                return (string) $this->getId();
×
262
        }
263

264

265
        /**
266
         * Tests if all necessary items are available to create the order.
267
         *
268
         * @param array $what Type of data
269
         * @return \Aimeos\MShop\Order\Item\Iface Order base item for method chaining
270
         * @throws \Aimeos\MShop\Order\Exception if there are no products in the basket
271
         */
272
        public function check( array $what = ['order/address', 'order/coupon', 'order/product', 'order/service'] ) : \Aimeos\MShop\Order\Item\Iface
273
        {
274
                $this->notify( 'check.before', $what );
3✔
275

276
                if( in_array( 'order/product', $what ) && ( count( $this->getProducts() ) < 1 ) ) {
3✔
277
                        throw new \Aimeos\MShop\Order\Exception( sprintf( 'Basket empty' ) );
2✔
278
                }
279

280
                $this->notify( 'check.after', $what );
1✔
281

282
                return $this;
1✔
283
        }
284

285

286
        /**
287
         * Notifies listeners before the basket becomes an order.
288
         *
289
         * @return \Aimeos\MShop\Order\Item\Iface Order base item for chaining method calls
290
         */
291
        public function finish() : \Aimeos\MShop\Order\Item\Iface
292
        {
293
                $this->notify( 'setOrder.before' );
×
294
                return $this;
×
295
        }
296

297

298
        /**
299
         * Adds the address of the given type to the basket
300
         *
301
         * @param \Aimeos\MShop\Order\Item\Address\Iface $address Order address item for the given type
302
         * @param string $type Address type, usually "billing" or "delivery"
303
         * @param int|null $position Position of the address in the list
304
         * @return \Aimeos\MShop\Order\Item\Iface Order base item for method chaining
305
         */
306
        public function addAddress( \Aimeos\MShop\Order\Item\Address\Iface $address, string $type, ?int $position = null ) : \Aimeos\MShop\Order\Item\Iface
307
        {
308
                $address = $this->notify( 'addAddress.before', $address );
52✔
309

310
                $address = clone $address;
52✔
311
                $address = $address->setType( $type );
52✔
312

313
                if( $position !== null ) {
52✔
314
                        $this->addresses[$type][$position] = $address;
1✔
315
                } else {
316
                        $this->addresses[$type][] = $address;
52✔
317
                }
318

319
                $this->price->setModified();
52✔
320
                $this->setModified();
52✔
321

322
                $this->notify( 'addAddress.after', $address );
52✔
323

324
                return $this;
52✔
325
        }
326

327

328
        /**
329
         * Deletes an order address from the basket
330
         *
331
         * @param string $type Address type defined in \Aimeos\MShop\Order\Item\Address\Base
332
         * @param int|null $position Position of the address in the list
333
         * @return \Aimeos\MShop\Order\Item\Iface Order base item for method chaining
334
         */
335
        public function deleteAddress( string $type, ?int $position = null ) : \Aimeos\MShop\Order\Item\Iface
336
        {
337
                if( $position === null && isset( $this->addresses[$type] ) || isset( $this->addresses[$type][$position] ) )
4✔
338
                {
339
                        $old = ( isset( $this->addresses[$type][$position] ) ? $this->addresses[$type][$position] : $this->addresses[$type] );
3✔
340
                        $old = $this->notify( 'deleteAddress.before', $old );
3✔
341

342
                        if( $position !== null ) {
3✔
343
                                unset( $this->addresses[$type][$position] );
1✔
344
                        } else {
345
                                unset( $this->addresses[$type] );
2✔
346
                        }
347

348
                        $this->price->setModified();
3✔
349
                        $this->setModified();
3✔
350

351
                        $this->notify( 'deleteAddress.after', $old );
3✔
352
                }
353

354
                return $this;
4✔
355
        }
356

357

358
        /**
359
         * Returns the order address depending on the given type
360
         *
361
         * @param string $type Address type, usually "billing" or "delivery"
362
         * @param int|null $position Address position in list of addresses
363
         * @return \Aimeos\MShop\Order\Item\Address\Iface[]|\Aimeos\MShop\Order\Item\Address\Iface Order address item or list of
364
         */
365
        public function getAddress( string $type, ?int $position = null )
366
        {
367
                if( $position !== null )
59✔
368
                {
369
                        if( isset( $this->addresses[$type][$position] ) ) {
4✔
370
                                return $this->addresses[$type][$position];
3✔
371
                        }
372

373
                        throw new \Aimeos\MShop\Order\Exception( sprintf( 'Address not available' ) );
1✔
374
                }
375

376
                return ( isset( $this->addresses[$type] ) ? $this->addresses[$type] : [] );
55✔
377
        }
378

379

380
        /**
381
         * Returns all addresses that are part of the basket
382
         *
383
         * @return \Aimeos\Map Associative list of address items implementing
384
         *  \Aimeos\MShop\Order\Item\Address\Iface with "billing" or "delivery" as key
385
         */
386
        public function getAddresses() : \Aimeos\Map
387
        {
388
                return map( $this->addresses );
30✔
389
        }
390

391

392
        /**
393
         * Replaces all addresses in the current basket with the new ones
394
         *
395
         * @param \Aimeos\Map|array $map Associative list of order addresses as returned by getAddresses()
396
         * @return \Aimeos\MShop\Order\Item\Iface Order base item for method chaining
397
         */
398
        public function setAddresses( iterable $map ) : \Aimeos\MShop\Order\Item\Iface
399
        {
400
                $map = $this->notify( 'setAddresses.before', $map );
7✔
401

402
                foreach( $map as $type => $items ) {
7✔
403
                        $this->checkAddresses( $items, $type );
7✔
404
                }
405

406
                $old = $this->addresses;
7✔
407
                $this->addresses = is_map( $map ) ? $map->toArray() : $map;
7✔
408

409
                $this->price->setModified();
7✔
410
                $this->setModified();
7✔
411

412
                $this->notify( 'setAddresses.after', $old );
7✔
413

414
                return $this;
7✔
415
        }
416

417

418
        /**
419
         * Adds a coupon code and the given product item to the basket
420
         *
421
         * @param string $code Coupon code
422
         * @return \Aimeos\MShop\Order\Item\Iface Order base item for method chaining
423
         */
424
        public function addCoupon( string $code ) : \Aimeos\MShop\Order\Item\Iface
425
        {
426
                if( !isset( $this->coupons[$code] ) )
3✔
427
                {
428
                        $code = $this->notify( 'addCoupon.before', $code );
3✔
429
                        $this->coupons[$code] = [];
3✔
430

431
                        $this->price->setModified();
3✔
432
                        $this->setModified();
3✔
433

434
                        $this->notify( 'addCoupon.after', $code );
3✔
435
                }
436

437
                return $this;
3✔
438
        }
439

440

441
        /**
442
         * Removes a coupon and the related product items from the basket
443
         *
444
         * @param string $code Coupon code
445
         * @return \Aimeos\MShop\Order\Item\Iface Order base item for method chaining
446
         */
447
        public function deleteCoupon( string $code ) : \Aimeos\MShop\Order\Item\Iface
448
        {
449
                if( isset( $this->coupons[$code] ) )
1✔
450
                {
451
                        $old = [$code => $this->coupons[$code]];
1✔
452
                        $old = $this->notify( 'deleteCoupon.before', $old );
1✔
453

454
                        foreach( $this->coupons[$code] as $product )
1✔
455
                        {
456
                                if( ( $key = array_search( $product, $this->products, true ) ) !== false ) {
1✔
457
                                        unset( $this->products[$key] );
1✔
458
                                }
459
                        }
460

461
                        unset( $this->coupons[$code] );
1✔
462

463
                        $this->price->setModified();
1✔
464
                        $this->setModified();
1✔
465

466
                        $this->notify( 'deleteCoupon.after', $old );
1✔
467
                }
468

469
                return $this;
1✔
470
        }
471

472

473
        /**
474
         * Returns the available coupon codes and the lists of affected product items
475
         *
476
         * @return \Aimeos\Map Associative array of codes and lists of product items
477
         *  implementing \Aimeos\MShop\Order\Product\Iface
478
         */
479
        public function getCoupons() : \Aimeos\Map
480
        {
481
                return map( $this->coupons );
76✔
482
        }
483

484

485
        /**
486
         * Sets a coupon code and the given product items in the basket.
487
         *
488
         * @param string $code Coupon code
489
         * @param \Aimeos\MShop\Order\Item\Product\Iface[] $products List of coupon products
490
         * @return \Aimeos\MShop\Order\Item\Iface Order base item for method chaining
491
         */
492
        public function setCoupon( string $code, iterable $products = [] ) : \Aimeos\MShop\Order\Item\Iface
493
        {
494
                $new = $this->notify( 'setCoupon.before', [$code => $products] );
17✔
495

496
                $products = $this->checkProducts( map( $new )->first( [] ) );
17✔
497

498
                if( isset( $this->coupons[$code] ) )
17✔
499
                {
500
                        foreach( $this->coupons[$code] as $product )
9✔
501
                        {
502
                                if( ( $key = array_search( $product, $this->products, true ) ) !== false ) {
1✔
503
                                        unset( $this->products[$key] );
1✔
504
                                }
505
                        }
506
                }
507

508
                foreach( $products as $product ) {
17✔
509
                        $this->products[] = $product;
15✔
510
                }
511

512
                $old = isset( $this->coupons[$code] ) ? [$code => $this->coupons[$code]] : [];
17✔
513
                $this->coupons[$code] = is_map( $products ) ? $products->toArray() : $products;
17✔
514

515
                $this->price->setModified();
17✔
516
                $this->setModified();
17✔
517

518
                $this->notify( 'setCoupon.after', $old );
17✔
519

520
                return $this;
17✔
521
        }
522

523

524
        /**
525
         * Replaces all coupons in the current basket with the new ones
526
         *
527
         * @param iterable $map Associative list of order coupons as returned by getCoupons()
528
         * @return \Aimeos\MShop\Order\Item\Iface Order base item for method chaining
529
         */
530
        public function setCoupons( iterable $map ) : \Aimeos\MShop\Order\Item\Iface
531
        {
532
                $map = $this->notify( 'setCoupons.before', $map );
3✔
533

534
                foreach( $map as $code => $products ) {
3✔
535
                        $map[$code] = $this->checkProducts( $products );
3✔
536
                }
537

538
                foreach( $this->coupons as $code => $products )
3✔
539
                {
540
                        foreach( $products as $product )
×
541
                        {
542
                                if( ( $key = array_search( $product, $this->products, true ) ) !== false ) {
×
543
                                        unset( $this->products[$key] );
×
544
                                }
545
                        }
546
                }
547

548
                foreach( $map as $code => $products )
3✔
549
                {
550
                        foreach( $products as $product ) {
3✔
551
                                $this->products[] = $product;
3✔
552
                        }
553
                }
554

555
                $old = $this->coupons;
3✔
556
                $this->coupons = is_map( $map ) ? $map->toArray() : $map;
3✔
557

558
                $this->price->setModified();
3✔
559
                $this->setModified();
3✔
560

561
                $this->notify( 'setCoupons.after', $old );
3✔
562

563
                return $this;
3✔
564
        }
565

566

567
        /**
568
         * Adds an order product item to the basket
569
         * If a similar item is found, only the quantity is increased.
570
         *
571
         * @param \Aimeos\MShop\Order\Item\Product\Iface $item Order product item to be added
572
         * @param int|null $position position of the new order product item
573
         * @return \Aimeos\MShop\Order\Item\Iface Order base item for method chaining
574
         */
575
        public function addProduct( \Aimeos\MShop\Order\Item\Product\Iface $item, ?int $position = null ) : \Aimeos\MShop\Order\Item\Iface
576
        {
577
                $item = $this->notify( 'addProduct.before', $item );
107✔
578

579
                $this->checkProducts( [$item] );
107✔
580

581
                if( $position !== null ) {
107✔
582
                        $this->products[$position] = $item;
8✔
583
                } elseif( ( $pos = $this->getSameProduct( $item, $this->products ) ) !== null ) {
105✔
584
                        $item = $this->products[$pos]->setQuantity( $this->products[$pos]->getQuantity() + $item->getQuantity() );
2✔
585
                } else {
586
                        $this->products[] = $item;
105✔
587
                }
588

589
                ksort( $this->products );
107✔
590

591
                $this->price->setModified();
107✔
592
                $this->setModified();
107✔
593

594
                $this->notify( 'addProduct.after', $item );
107✔
595

596
                return $this;
107✔
597
        }
598

599

600
        /**
601
         * Deletes an order product item from the basket
602
         *
603
         * @param int $position Position of the order product item
604
         * @return \Aimeos\MShop\Order\Item\Iface Order base item for method chaining
605
         */
606
        public function deleteProduct( int $position ) : \Aimeos\MShop\Order\Item\Iface
607
        {
608
                if( isset( $this->products[$position] ) )
4✔
609
                {
610
                        $old = $this->products[$position];
4✔
611
                        $old = $this->notify( 'deleteProduct.before', $old );
4✔
612

613
                        unset( $this->products[$position] );
4✔
614

615
                        $this->price->setModified();
4✔
616
                        $this->setModified();
4✔
617

618
                        $this->notify( 'deleteProduct.after', $old );
4✔
619
                }
620

621
                return $this;
4✔
622
        }
623

624

625
        /**
626
         * Returns the product item of an basket specified by its key
627
         *
628
         * @param int $key Key returned by getProducts() identifying the requested product
629
         * @return \Aimeos\MShop\Order\Item\Product\Iface Product item of an order
630
         */
631
        public function getProduct( int $key ) : \Aimeos\MShop\Order\Item\Product\Iface
632
        {
633
                if( !isset( $this->products[$key] ) ) {
13✔
634
                        throw new \Aimeos\MShop\Order\Exception( sprintf( 'Product not available' ) );
×
635
                }
636

637
                return $this->products[$key];
13✔
638
        }
639

640

641
        /**
642
         * Returns the product items that are or should be part of a basket
643
         *
644
         * @return \Aimeos\Map List of order product items implementing \Aimeos\MShop\Order\Item\Product\Iface
645
         */
646
        public function getProducts() : \Aimeos\Map
647
        {
648
                return map( $this->products );
144✔
649
        }
650

651

652
        /**
653
         * Replaces all products in the current basket with the new ones
654
         *
655
         * @param \Aimeos\MShop\Order\Item\Product\Iface[] $map Associative list of ordered products as returned by getProducts()
656
         * @return \Aimeos\MShop\Order\Item\Iface Order base item for method chaining
657
         */
658
        public function setProducts( iterable $map ) : \Aimeos\MShop\Order\Item\Iface
659
        {
660
                $map = $this->notify( 'setProducts.before', $map );
7✔
661

662
                $this->checkProducts( $map );
7✔
663

664
                $old = $this->products;
7✔
665
                $this->products = is_map( $map ) ? $map->toArray() : $map;
7✔
666

667
                $this->price->setModified();
7✔
668
                $this->setModified();
7✔
669

670
                $this->notify( 'setProducts.after', $old );
7✔
671

672
                return $this;
7✔
673
        }
674

675

676
        /**
677
         * Adds an order service to the basket
678
         *
679
         * @param \Aimeos\MShop\Order\Item\Service\Iface $service Order service item for the given domain
680
         * @param string $type Service type constant from \Aimeos\MShop\Order\Item\Service\Base
681
         * @param int|null $position Position of the service in the list to overwrite
682
         * @return \Aimeos\MShop\Order\Item\Iface Order base item for method chaining
683
         */
684
        public function addService( \Aimeos\MShop\Order\Item\Service\Iface $service, string $type, ?int $position = null ) : \Aimeos\MShop\Order\Item\Iface
685
        {
686
                $service = $this->notify( 'addService.before', $service );
23✔
687

688
                $this->checkPrice( $service->getPrice() );
23✔
689

690
                $service = clone $service;
23✔
691
                $service = $service->setType( $type );
23✔
692

693
                if( $position !== null ) {
23✔
694
                        $this->services[$type][$position] = $service;
1✔
695
                } else {
696
                        $this->services[$type][] = $service;
23✔
697
                }
698

699
                $this->price->setModified();
23✔
700
                $this->setModified();
23✔
701

702
                $this->notify( 'addService.after', $service );
23✔
703

704
                return $this;
23✔
705
        }
706

707

708
        /**
709
         * Deletes an order service from the basket
710
         *
711
         * @param string $type Service type constant from \Aimeos\MShop\Order\Item\Service\Base
712
         * @param int|null $position Position of the service in the list to delete
713
         * @return \Aimeos\MShop\Order\Item\Iface Order base item for method chaining
714
         */
715
        public function deleteService( string $type, ?int $position = null ) : \Aimeos\MShop\Order\Item\Iface
716
        {
717
                if( $position === null && isset( $this->services[$type] ) || isset( $this->services[$type][$position] ) )
4✔
718
                {
719
                        $old = ( isset( $this->services[$type][$position] ) ? $this->services[$type][$position] : $this->services[$type] );
3✔
720
                        $old = $this->notify( 'deleteService.before', $old );
3✔
721

722
                        if( $position !== null ) {
3✔
723
                                unset( $this->services[$type][$position] );
1✔
724
                        } else {
725
                                unset( $this->services[$type] );
2✔
726
                        }
727

728
                        $this->price->setModified();
3✔
729
                        $this->setModified();
3✔
730

731
                        $this->notify( 'deleteService.after', $old );
3✔
732
                }
733

734
                return $this;
4✔
735
        }
736

737

738
        /**
739
         * Returns the order services depending on the given type
740
         *
741
         * @param string $type Service type constant from \Aimeos\MShop\Order\Item\Service\Base
742
         * @param int|null $position Position of the service in the list to retrieve
743
         * @return \Aimeos\MShop\Order\Item\Service\Iface[]|\Aimeos\MShop\Order\Item\Service\Iface
744
         *         Order service item or list of items for the requested type
745
         * @throws \Aimeos\MShop\Order\Exception If no service for the given type and position is found
746
         */
747
        public function getService( string $type, ?int $position = null )
748
        {
749
                if( $position !== null )
47✔
750
                {
751
                        if( isset( $this->services[$type][$position] ) ) {
20✔
752
                                return $this->services[$type][$position];
19✔
753
                        }
754

755
                        throw new \Aimeos\MShop\Order\Exception( sprintf( 'Service not available' ) );
1✔
756
                }
757

758
                return ( isset( $this->services[$type] ) ? $this->services[$type] : [] );
37✔
759
        }
760

761

762
        /**
763
         * Returns all services that are part of the basket
764
         *
765
         * @return \Aimeos\Map Associative list of service types ("delivery" or "payment") as keys and list of
766
         *        service items implementing \Aimeos\MShop\Order\Service\Iface as values
767
         */
768
        public function getServices() : \Aimeos\Map
769
        {
770
                return map( $this->services );
94✔
771
        }
772

773

774
        /**
775
         * Replaces all services in the current basket with the new ones
776
         *
777
         * @param \Aimeos\MShop\Order\Item\Service\Iface[] $map Associative list of order services as returned by getServices()
778
         * @return \Aimeos\MShop\Order\Item\Iface Order base item for method chaining
779
         */
780
        public function setServices( iterable $map ) : \Aimeos\MShop\Order\Item\Iface
781
        {
782
                $map = $this->notify( 'setServices.before', $map );
14✔
783

784
                foreach( $map as $type => $services ) {
14✔
785
                        $map[$type] = $this->checkServices( $services, $type );
14✔
786
                }
787

788
                $old = $this->services;
14✔
789
                $this->services = is_map( $map ) ? $map->toArray() : $map;
14✔
790

791
                $this->price->setModified();
14✔
792
                $this->setModified();
14✔
793

794
                $this->notify( 'setServices.after', $old );
14✔
795

796
                return $this;
14✔
797
        }
798

799

800
        /**
801
         * Adds a status item to the order
802
         *
803
         * @param \Aimeos\MShop\Order\Item\Status\Iface $item Order status item
804
         * @return \Aimeos\MShop\Order\Item\Iface Order item for method chaining
805
         */
806
        public function addStatus( \Aimeos\MShop\Order\Item\Status\Iface $item ) : \Aimeos\MShop\Order\Item\Iface
807
        {
808
                $type = $item->getType();
5✔
809
                $value = $item->getValue();
5✔
810

811
                if( isset( $this->statuses[$type][$value] ) ) {
5✔
812
                        $this->statuses[$type][$value] = $item->setId( $this->statuses[$type][$value]->getId() );
×
813
                } else {
814
                        $this->statuses[$type][$value] = $item;
5✔
815
                }
816

817
                return $this;
5✔
818
        }
819

820

821
        /**
822
         * Returns the status item specified by its type and value
823
         *
824
         * @param string $type Status type
825
         * @param string $value Status value
826
         * @return \Aimeos\MShop\Order\Item\Status\Iface Status item of an order
827
         * @throws \Aimeos\MShop\Order\Exception If status item is not available
828
         */
829
        public function getStatus( string $type, string $value ) : \Aimeos\MShop\Order\Item\Status\Iface
830
        {
831
                if( !isset( $this->statuses[$type][$value] ) ) {
1✔
832
                        throw new \Aimeos\MShop\Order\Exception( sprintf( 'Status not available' ) );
×
833
                }
834

835
                return $this->statuses[$type][$value];
1✔
836
        }
837

838

839
        /**
840
         * Returns the status items
841
         *
842
         * @return \Aimeos\Map Associative list of status types as keys and list of
843
         *        status value/item pairs implementing \Aimeos\MShop\Order\Status\Iface as values
844
         */
845
        public function getStatuses() : \Aimeos\Map
846
        {
847
                return map( $this->statuses );
12✔
848
        }
849

850

851
        /**
852
         * Returns the service costs
853
         *
854
         * @param string $type Service type like "delivery" or "payment"
855
         * @return float Service costs value
856
         */
857
        public function getCosts( string $type = 'delivery' ) : float
858
        {
859
                $costs = 0;
1✔
860

861
                foreach( $this->getService( $type ) as $service ) {
1✔
862
                        $costs += $service->getPrice()->getCosts();
1✔
863
                }
864

865
                return $costs;
1✔
866
        }
867

868

869
        /**
870
         * Returns a price item with amounts calculated for the products, costs, etc.
871
         *
872
         * @return \Aimeos\MShop\Price\Item\Iface Price item with price, costs and rebate the customer has to pay
873
         */
874
        public function getPrice() : \Aimeos\MShop\Price\Item\Iface
875
        {
876
                if( $this->price->isModified() )
178✔
877
                {
878
                        $price = $this->price->clear();
64✔
879

880
                        foreach( $this->getServices() as $list )
64✔
881
                        {
882
                                foreach( $list as $service ) {
12✔
883
                                        $price = $price->addItem( $service->getPrice() );
12✔
884
                                }
885
                        }
886

887
                        foreach( $this->getProducts() as $product ) {
64✔
888
                                $price = $price->addItem( $product->getPrice(), $product->getQuantity() );
45✔
889
                        }
890

891
                        $this->price = $price->setId( '' ); // clear modified flag
64✔
892
                }
893

894
                return $this->price;
178✔
895
        }
896

897

898
        /**
899
         * Returns a list of tax names and values
900
         *
901
         * @return array Associative list of tax names as key and price items as value
902
         */
903
        public function getTaxes() : array
904
        {
905
                $taxes = [];
1✔
906

907
                foreach( $this->getProducts() as $product )
1✔
908
                {
909
                        $price = $product->getPrice();
1✔
910

911
                        foreach( $price->getTaxrates() as $name => $taxrate )
1✔
912
                        {
913
                                $price = ( clone $price )->setTaxRate( $taxrate );
1✔
914

915
                                if( isset( $taxes[$name][$taxrate] ) ) {
1✔
916
                                        $taxes[$name][$taxrate]->addItem( $price, $product->getQuantity() );
×
917
                                } else {
918
                                        $taxes[$name][$taxrate] = $price->addItem( $price, $product->getQuantity() - 1 );
1✔
919
                                }
920
                        }
921
                }
922

923
                foreach( $this->getServices() as $services )
1✔
924
                {
925
                        foreach( $services as $service )
1✔
926
                        {
927
                                $price = $service->getPrice();
1✔
928

929
                                foreach( $price->getTaxrates() as $name => $taxrate )
1✔
930
                                {
931
                                        $price = ( clone $price )->setTaxRate( $taxrate );
1✔
932

933
                                        if( isset( $taxes[$name][$taxrate] ) ) {
1✔
934
                                                $taxes[$name][$taxrate]->addItem( $price );
1✔
935
                                        } else {
936
                                                $taxes[$name][$taxrate] = $price;
×
937
                                        }
938
                                }
939
                        }
940
                }
941

942
                return $taxes;
1✔
943
        }
944

945

946
        /**
947
         * Returns the locales for the basic order item.
948
         *
949
         * @return \Aimeos\MShop\Locale\Item\Iface Object containing information
950
         *  about site, language, country and currency
951
         */
952
        public function locale() : \Aimeos\MShop\Locale\Item\Iface
953
        {
954
                return $this->locale;
21✔
955
        }
956

957

958
        /**
959
         * Sets the locales for the basic order item.
960
         *
961
         * @param \Aimeos\MShop\Locale\Item\Iface $locale Object containing information
962
         *  about site, language, country and currency
963
         * @return \Aimeos\MShop\Order\Item\Iface Order base item for chaining method calls
964
         */
965
        public function setLocale( \Aimeos\MShop\Locale\Item\Iface $locale ) : \Aimeos\MShop\Order\Item\Iface
966
        {
967
                $this->notify( 'setLocale.before', $locale );
2✔
968
                $this->locale = clone $locale;
2✔
969
                $this->notify( 'setLocale.after', $locale );
2✔
970

971
                return $this->setModified();
2✔
972
        }
973

974

975
        /**
976
         * Returns the item values as array.
977
         *
978
         * @param bool True to return private properties, false for public only
979
         * @return array Associative list of item properties and their values
980
         */
981
        public function toArray( bool $private = false ) : array
982
        {
983
                $price = $this->getPrice();
14✔
984
                $list = parent::toArray( $private );
14✔
985

986
                $list['order.currencyid'] = $price->getCurrencyId();
14✔
987
                $list['order.price'] = $price->getValue();
14✔
988
                $list['order.costs'] = $price->getCosts();
14✔
989
                $list['order.rebate'] = $price->getRebate();
14✔
990
                $list['order.taxflag'] = $price->getTaxFlag();
14✔
991
                $list['order.taxvalue'] = $price->getTaxValue();
14✔
992

993
                return $list;
14✔
994
        }
995

996

997
        /**
998
         * Checks if the price uses the same currency as the price in the basket.
999
         *
1000
         * @param \Aimeos\MShop\Price\Item\Iface $item Price item
1001
         */
1002
        protected function checkPrice( \Aimeos\MShop\Price\Item\Iface $item )
1003
        {
1004
                $price = clone $this->getPrice();
145✔
1005
                $price->addItem( $item );
145✔
1006
        }
1007

1008

1009
        /**
1010
         * Checks if all order addresses are valid
1011
         *
1012
         * @param \Aimeos\MShop\Order\Item\Address\Iface[] $items Order address items
1013
         * @param string $type Address type constant from \Aimeos\MShop\Order\Item\Address\Base
1014
         * @return \Aimeos\MShop\Order\Item\Address\Iface[] List of checked items
1015
         * @throws \Aimeos\MShop\Exception If one of the order addresses is invalid
1016
         */
1017
        protected function checkAddresses( iterable $items, string $type ) : iterable
1018
        {
1019
                map( $items )->implements( \Aimeos\MShop\Order\Item\Address\Iface::class, true );
7✔
1020

1021
                foreach( $items as $key => $item ) {
7✔
1022
                        $items[$key] = $item->setType( $type );
7✔
1023
                }
1024

1025
                return $items;
7✔
1026
        }
1027

1028

1029
        /**
1030
         * Checks if all order products are valid
1031
         *
1032
         * @param \Aimeos\MShop\Order\Item\Product\Iface[] $items Order product items
1033
         * @return \Aimeos\MShop\Order\Item\Product\Iface[] List of checked items
1034
         * @throws \Aimeos\MShop\Exception If one of the order products is invalid
1035
         */
1036
        protected function checkProducts( iterable $items ) : \Aimeos\Map
1037
        {
1038
                map( $items )->implements( \Aimeos\MShop\Order\Item\Product\Iface::class, true );
119✔
1039

1040
                foreach( $items as $key => $item )
119✔
1041
                {
1042
                        if( $item->getProductCode() === '' ) {
117✔
UNCOV
1043
                                throw new \Aimeos\MShop\Order\Exception( sprintf( 'Product does not contain the SKU code' ) );
×
1044
                        }
1045

1046
                        $this->checkPrice( $item->getPrice() );
117✔
1047
                }
1048

1049
                return map( $items );
119✔
1050
        }
1051

1052

1053
        /**
1054
         * Checks if all order services are valid
1055
         *
1056
         * @param \Aimeos\MShop\Order\Item\Service\Iface[] $items Order service items
1057
         * @param string $type Service type constant from \Aimeos\MShop\Order\Item\Service\Base
1058
         * @return \Aimeos\MShop\Order\Item\Service\Iface[] List of checked items
1059
         * @throws \Aimeos\MShop\Exception If one of the order services is invalid
1060
         */
1061
        protected function checkServices( iterable $items, string $type ) : iterable
1062
        {
1063
                map( $items )->implements( \Aimeos\MShop\Order\Item\Service\Iface::class, true );
14✔
1064

1065
                foreach( $items as $key => $item )
14✔
1066
                {
1067
                        $this->checkPrice( $item->getPrice() );
12✔
1068
                        $items[$key] = $item->setType( $type );
12✔
1069
                }
1070

1071
                return $items;
14✔
1072
        }
1073

1074

1075
        /**
1076
         * Tests if the given product is similar to an existing one.
1077
         * Similarity is described by the equality of properties so the quantity of
1078
         * the existing product can be updated.
1079
         *
1080
         * @param \Aimeos\MShop\Order\Item\Product\Iface $item Order product item
1081
         * @param \Aimeos\MShop\Order\Item\Product\Iface[] $products List of order product items to check against
1082
         * @return int|null Positon of the same product in the product list of false if product is unique
1083
         * @throws \Aimeos\MShop\Order\Exception If no similar item was found
1084
         */
1085
        protected function getSameProduct( \Aimeos\MShop\Order\Item\Product\Iface $item, iterable $products ) : ?int
1086
        {
1087
                $map = [];
105✔
1088
                $count = 0;
105✔
1089

1090
                foreach( $item->getAttributeItems() as $attributeItem )
105✔
1091
                {
1092
                        $key = md5( $attributeItem->getCode() . json_encode( $attributeItem->getValue() ) );
11✔
1093
                        $map[$key] = $attributeItem;
11✔
1094
                        $count++;
11✔
1095
                }
1096

1097
                foreach( $products as $position => $product )
105✔
1098
                {
1099
                        if( $product->compare( $item ) === false ) {
19✔
1100
                                continue;
18✔
1101
                        }
1102

1103
                        $prodAttributes = $product->getAttributeItems();
2✔
1104

1105
                        if( count( $prodAttributes ) !== $count ) {
2✔
UNCOV
1106
                                continue;
×
1107
                        }
1108

1109
                        foreach( $prodAttributes as $attribute )
2✔
1110
                        {
UNCOV
1111
                                $key = md5( $attribute->getCode() . json_encode( $attribute->getValue() ) );
×
1112

UNCOV
1113
                                if( isset( $map[$key] ) === false || $map[$key]->getQuantity() != $attribute->getQuantity() ) {
×
UNCOV
1114
                                        continue 2; // jump to outer loop
×
1115
                                }
1116
                        }
1117

1118
                        return $position;
2✔
1119
                }
1120

1121
                return null;
105✔
1122
        }
1123
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc