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

aimeos / aimeos-core / 1c6482b1-b3b2-437c-a796-9d13ce51d2ed

18 Aug 2024 10:51AM UTC coverage: 91.989% (+0.007%) from 91.982%
1c6482b1-b3b2-437c-a796-9d13ce51d2ed

push

circleci

aimeos
Pass objects to order item in values array

84 of 87 new or added lines in 3 files covered. (96.55%)

29 existing lines in 2 files now uncovered.

10208 of 11097 relevant lines covered (91.99%)

64.73 hits per line

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

90.74
/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-2024
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 bool $recalc = false;
151
        protected array $coupons = [];
152
        protected array $products = [];
153
        protected array $services = [];
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
                parent::__construct( $prefix, $values );
542✔
166

167
                $this->customer = $values['.customer'] ?? null;
542✔
168
                $this->locale = $values['.locale'];
542✔
169
                $this->price = $values['.price'];
542✔
170

171
                $products = $this->get( '.products', [] );
542✔
172

173
                foreach( $this->get( '.coupons', [] ) as $coupon )
542✔
174
                {
175
                        if( !isset( $this->coupons[$coupon->getCode()] ) ) {
6✔
176
                                $this->coupons[$coupon->getCode()] = [];
6✔
177
                        }
178

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

184
                foreach( $this->get( '.products', [] ) as $product ) {
542✔
185
                        $this->products[$product->getPosition()] = $product;
38✔
186
                }
187

188
                foreach( $this->get( '.addresses', [] ) as $address ) {
542✔
189
                        $this->addresses[$address->getType()][] = $address;
32✔
190
                }
191

192
                foreach( $this->get( '.services', [] ) as $service ) {
542✔
193
                        $this->services[$service->getType()][] = $service;
33✔
194
                }
195
        }
196

197

198
        /**
199
         * Clones internal objects of the order item.
200
         */
201
        public function __clone()
202
        {
203
                parent::__clone();
3✔
204

205
                $this->price = clone $this->price;
3✔
206
                $this->locale = clone $this->locale;
3✔
207
        }
208

209

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

229

230
        /**
231
         * Prepares the object for serialization.
232
         *
233
         * @return array List of properties that should be serialized
234
         */
235
        public function __sleep() : array
236
        {
237
                /*
238
                 * Workaround because database connections can't be serialized
239
                 * Listeners will be reattached on wakeup by the order base manager
240
                 */
241
                $this->off();
2✔
242

243
                return array_keys( get_object_vars( $this ) );
2✔
244
        }
245

246

247
        /**
248
         * Returns the ID of the items
249
         *
250
         * @return string ID of the item or null
251
         */
252
        public function __toString() : string
253
        {
UNCOV
254
                return (string) $this->getId();
×
255
        }
256

257

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

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

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

275
                return $this;
1✔
276
        }
277

278

279
        /**
280
         * Notifies listeners before the basket becomes an order.
281
         *
282
         * @return \Aimeos\MShop\Order\Item\Iface Order base item for chaining method calls
283
         */
284
        public function finish() : \Aimeos\MShop\Order\Item\Iface
285
        {
UNCOV
286
                $this->notify( 'setOrder.before' );
×
UNCOV
287
                return $this;
×
288
        }
289

290

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

303
                $address = clone $address;
52✔
304
                $address = $address->setType( $type );
52✔
305

306
                if( $position !== null ) {
52✔
307
                        $this->addresses[$type][$position] = $address;
1✔
308
                } else {
309
                        $this->addresses[$type][] = $address;
52✔
310
                }
311

312
                $this->setModified();
52✔
313

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

316
                return $this;
52✔
317
        }
318

319

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

334
                        if( $position !== null ) {
3✔
335
                                unset( $this->addresses[$type][$position] );
1✔
336
                        } else {
337
                                unset( $this->addresses[$type] );
2✔
338
                        }
339

340
                        $this->setModified();
3✔
341

342
                        $this->notify( 'deleteAddress.after', $old );
3✔
343
                }
344

345
                return $this;
4✔
346
        }
347

348

349
        /**
350
         * Returns the order address depending on the given type
351
         *
352
         * @param string $type Address type, usually "billing" or "delivery"
353
         * @param int|null $position Address position in list of addresses
354
         * @return \Aimeos\MShop\Order\Item\Address\Iface[]|\Aimeos\MShop\Order\Item\Address\Iface Order address item or list of
355
         */
356
        public function getAddress( string $type, int $position = null )
357
        {
358
                if( $position !== null )
59✔
359
                {
360
                        if( isset( $this->addresses[$type][$position] ) ) {
4✔
361
                                return $this->addresses[$type][$position];
3✔
362
                        }
363

364
                        throw new \Aimeos\MShop\Order\Exception( sprintf( 'Address not available' ) );
1✔
365
                }
366

367
                return ( isset( $this->addresses[$type] ) ? $this->addresses[$type] : [] );
55✔
368
        }
369

370

371
        /**
372
         * Returns all addresses that are part of the basket
373
         *
374
         * @return \Aimeos\Map Associative list of address items implementing
375
         *  \Aimeos\MShop\Order\Item\Address\Iface with "billing" or "delivery" as key
376
         */
377
        public function getAddresses() : \Aimeos\Map
378
        {
379
                return map( $this->addresses );
29✔
380
        }
381

382

383
        /**
384
         * Returns all address items as plain list
385
         *
386
         * @return \Aimeos\Map Associative list of address items implementing \Aimeos\MShop\Order\Item\Address\Iface
387
         */
388
        public function getAddressItems() : \Aimeos\Map
389
        {
UNCOV
390
                return map( $this->addresses )->flat( 1 );
×
391
        }
392

393

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

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

408
                $old = $this->addresses;
7✔
409
                $this->addresses = is_map( $map ) ? $map->toArray() : $map;
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

430
                        $this->coupons[$code] = [];
3✔
431
                        $this->setModified();
3✔
432

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

436
                return $this;
3✔
437
        }
438

439

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

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

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

463
                        $this->notify( 'deleteCoupon.after', $old );
1✔
464
                }
465

466
                return $this;
1✔
467
        }
468

469

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

481

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

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

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

505
                foreach( $products as $product ) {
17✔
506
                        $this->products[] = $product;
15✔
507
                }
508

509
                $old = isset( $this->coupons[$code] ) ? [$code => $this->coupons[$code]] : [];
17✔
510
                $this->coupons[$code] = is_map( $products ) ? $products->toArray() : $products;
17✔
511
                $this->setModified();
17✔
512

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

515
                return $this;
17✔
516
        }
517

518

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

529
                foreach( $map as $code => $products ) {
3✔
530
                        $map[$code] = $this->checkProducts( $products );
3✔
531
                }
532

533
                foreach( $this->coupons as $code => $products )
3✔
534
                {
UNCOV
535
                        foreach( $products as $product )
×
536
                        {
UNCOV
537
                                if( ( $key = array_search( $product, $this->products, true ) ) !== false ) {
×
UNCOV
538
                                        unset( $this->products[$key] );
×
539
                                }
540
                        }
541
                }
542

543
                foreach( $map as $code => $products )
3✔
544
                {
545
                        foreach( $products as $product ) {
3✔
546
                                $this->products[] = $product;
3✔
547
                        }
548
                }
549

550
                $old = $this->coupons;
3✔
551
                $this->coupons = is_map( $map ) ? $map->toArray() : $map;
3✔
552
                $this->setModified();
3✔
553

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

556
                return $this;
3✔
557
        }
558

559

560
        /**
561
         * Adds an order product item to the basket
562
         * If a similar item is found, only the quantity is increased.
563
         *
564
         * @param \Aimeos\MShop\Order\Item\Product\Iface $item Order product item to be added
565
         * @param int|null $position position of the new order product item
566
         * @return \Aimeos\MShop\Order\Item\Iface Order base item for method chaining
567
         */
568
        public function addProduct( \Aimeos\MShop\Order\Item\Product\Iface $item, int $position = null ) : \Aimeos\MShop\Order\Item\Iface
569
        {
570
                $item = $this->notify( 'addProduct.before', $item );
106✔
571

572
                $this->checkProducts( [$item] );
106✔
573

574
                if( $position !== null ) {
106✔
575
                        $this->products[$position] = $item;
8✔
576
                } elseif( ( $pos = $this->getSameProduct( $item, $this->products ) ) !== null ) {
104✔
577
                        $item = $this->products[$pos]->setQuantity( $this->products[$pos]->getQuantity() + $item->getQuantity() );
2✔
578
                } else {
579
                        $this->products[] = $item;
104✔
580
                }
581

582
                ksort( $this->products );
106✔
583
                $this->setModified();
106✔
584

585
                $this->notify( 'addProduct.after', $item );
106✔
586

587
                return $this;
106✔
588
        }
589

590

591
        /**
592
         * Deletes an order product item from the basket
593
         *
594
         * @param int $position Position of the order product item
595
         * @return \Aimeos\MShop\Order\Item\Iface Order base item for method chaining
596
         */
597
        public function deleteProduct( int $position ) : \Aimeos\MShop\Order\Item\Iface
598
        {
599
                if( isset( $this->products[$position] ) )
4✔
600
                {
601
                        $old = $this->products[$position];
4✔
602
                        $old = $this->notify( 'deleteProduct.before', $old );
4✔
603

604
                        unset( $this->products[$position] );
4✔
605
                        $this->setModified();
4✔
606

607
                        $this->notify( 'deleteProduct.after', $old );
4✔
608
                }
609

610
                return $this;
4✔
611
        }
612

613

614
        /**
615
         * Returns the product item of an basket specified by its key
616
         *
617
         * @param int $key Key returned by getProducts() identifying the requested product
618
         * @return \Aimeos\MShop\Order\Item\Product\Iface Product item of an order
619
         */
620
        public function getProduct( int $key ) : \Aimeos\MShop\Order\Item\Product\Iface
621
        {
622
                if( !isset( $this->products[$key] ) ) {
13✔
UNCOV
623
                        throw new \Aimeos\MShop\Order\Exception( sprintf( 'Product not available' ) );
×
624
                }
625

626
                return $this->products[$key];
13✔
627
        }
628

629

630
        /**
631
         * Returns the product items that are or should be part of a basket
632
         *
633
         * @return \Aimeos\Map List of order product items implementing \Aimeos\MShop\Order\Item\Product\Iface
634
         */
635
        public function getProducts() : \Aimeos\Map
636
        {
637
                return map( $this->products );
141✔
638
        }
639

640

641
        /**
642
         * Replaces all products in the current basket with the new ones
643
         *
644
         * @param \Aimeos\MShop\Order\Item\Product\Iface[] $map Associative list of ordered products as returned by getProducts()
645
         * @return \Aimeos\MShop\Order\Item\Iface Order base item for method chaining
646
         */
647
        public function setProducts( iterable $map ) : \Aimeos\MShop\Order\Item\Iface
648
        {
649
                $map = $this->notify( 'setProducts.before', $map );
8✔
650

651
                $this->checkProducts( $map );
8✔
652

653
                $old = $this->products;
8✔
654
                $this->products = is_map( $map ) ? $map->toArray() : $map;
8✔
655
                $this->setModified();
8✔
656

657
                $this->notify( 'setProducts.after', $old );
8✔
658

659
                return $this;
8✔
660
        }
661

662

663
        /**
664
         * Adds an order service to the basket
665
         *
666
         * @param \Aimeos\MShop\Order\Item\Service\Iface $service Order service item for the given domain
667
         * @param string $type Service type constant from \Aimeos\MShop\Order\Item\Service\Base
668
         * @param int|null $position Position of the service in the list to overwrite
669
         * @return \Aimeos\MShop\Order\Item\Iface Order base item for method chaining
670
         */
671
        public function addService( \Aimeos\MShop\Order\Item\Service\Iface $service, string $type, int $position = null ) : \Aimeos\MShop\Order\Item\Iface
672
        {
673
                $service = $this->notify( 'addService.before', $service );
23✔
674

675
                $this->checkPrice( $service->getPrice() );
23✔
676

677
                $service = clone $service;
23✔
678
                $service = $service->setType( $type );
23✔
679

680
                if( $position !== null ) {
23✔
681
                        $this->services[$type][$position] = $service;
1✔
682
                } else {
683
                        $this->services[$type][] = $service;
23✔
684
                }
685

686
                $this->setModified();
23✔
687

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

690
                return $this;
23✔
691
        }
692

693

694
        /**
695
         * Deletes an order service from the basket
696
         *
697
         * @param string $type Service type constant from \Aimeos\MShop\Order\Item\Service\Base
698
         * @param int|null $position Position of the service in the list to delete
699
         * @return \Aimeos\MShop\Order\Item\Iface Order base item for method chaining
700
         */
701
        public function deleteService( string $type, int $position = null ) : \Aimeos\MShop\Order\Item\Iface
702
        {
703
                if( $position === null && isset( $this->services[$type] ) || isset( $this->services[$type][$position] ) )
4✔
704
                {
705
                        $old = ( isset( $this->services[$type][$position] ) ? $this->services[$type][$position] : $this->services[$type] );
3✔
706
                        $old = $this->notify( 'deleteService.before', $old );
3✔
707

708
                        if( $position !== null ) {
3✔
709
                                unset( $this->services[$type][$position] );
1✔
710
                        } else {
711
                                unset( $this->services[$type] );
2✔
712
                        }
713

714
                        $this->setModified();
3✔
715

716
                        $this->notify( 'deleteService.after', $old );
3✔
717
                }
718

719
                return $this;
4✔
720
        }
721

722

723
        /**
724
         * Returns the order services depending on the given type
725
         *
726
         * @param string $type Service type constant from \Aimeos\MShop\Order\Item\Service\Base
727
         * @param int|null $position Position of the service in the list to retrieve
728
         * @return \Aimeos\MShop\Order\Item\Service\Iface[]|\Aimeos\MShop\Order\Item\Service\Iface
729
         *         Order service item or list of items for the requested type
730
         * @throws \Aimeos\MShop\Order\Exception If no service for the given type and position is found
731
         */
732
        public function getService( string $type, int $position = null )
733
        {
734
                if( $position !== null )
47✔
735
                {
736
                        if( isset( $this->services[$type][$position] ) ) {
20✔
737
                                return $this->services[$type][$position];
19✔
738
                        }
739

740
                        throw new \Aimeos\MShop\Order\Exception( sprintf( 'Service not available' ) );
1✔
741
                }
742

743
                return ( isset( $this->services[$type] ) ? $this->services[$type] : [] );
37✔
744
        }
745

746

747
        /**
748
         * Returns all services that are part of the basket
749
         *
750
         * @return \Aimeos\Map Associative list of service types ("delivery" or "payment") as keys and list of
751
         *        service items implementing \Aimeos\MShop\Order\Service\Iface as values
752
         */
753
        public function getServices() : \Aimeos\Map
754
        {
755
                return map( $this->services );
92✔
756
        }
757

758

759
        /**
760
         * Returns all service items as        plain list
761
         *
762
         * @return \Aimeos\Map List of service items implementing \Aimeos\MShop\Order\Service\Iface
763
         */
764
        public function getServiceItems() : \Aimeos\Map
765
        {
UNCOV
766
                return map( $this->services )->flat( 1 );
×
767
        }
768

769

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

780
                foreach( $map as $type => $services ) {
14✔
781
                        $map[$type] = $this->checkServices( $services, $type );
14✔
782
                }
783

784
                $old = $this->services;
14✔
785
                $this->services = is_map( $map ) ? $map->toArray() : $map;
14✔
786
                $this->setModified();
14✔
787

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

790
                return $this;
14✔
791
        }
792

793

794
        /**
795
         * Returns the service costs
796
         *
797
         * @param string $type Service type like "delivery" or "payment"
798
         * @return float Service costs value
799
         */
800
        public function getCosts( string $type = 'delivery' ) : float
801
        {
802
                $costs = 0;
1✔
803

804
                if( $type === 'delivery' )
1✔
805
                {
806
                        foreach( $this->getProducts() as $product ) {
1✔
807
                                $costs += $product->getPrice()->getCosts() * $product->getQuantity();
1✔
808
                        }
809
                }
810

811
                foreach( $this->getService( $type ) as $service ) {
1✔
812
                        $costs += $service->getPrice()->getCosts();
1✔
813
                }
814

815
                return $costs;
1✔
816
        }
817

818

819
        /**
820
         * Returns a price item with amounts calculated for the products, costs, etc.
821
         *
822
         * @return \Aimeos\MShop\Price\Item\Iface Price item with price, costs and rebate the customer has to pay
823
         */
824
        public function getPrice() : \Aimeos\MShop\Price\Item\Iface
825
        {
826
                if( $this->recalc )
176✔
827
                {
828
                        $price = $this->price->clear();
66✔
829

830
                        foreach( $this->getServices() as $list )
66✔
831
                        {
832
                                foreach( $list as $service ) {
12✔
833
                                        $price = $price->addItem( $service->getPrice() );
12✔
834
                                }
835
                        }
836

837
                        foreach( $this->getProducts() as $product ) {
66✔
838
                                $price = $price->addItem( $product->getPrice(), $product->getQuantity() );
46✔
839
                        }
840

841
                        $this->price = $price;
66✔
842
                        $this->recalc = false;
66✔
843
                }
844

845
                return $this->price;
176✔
846
        }
847

848

849
        /**
850
         * Returns a list of tax names and values
851
         *
852
         * @return array Associative list of tax names as key and price items as value
853
         */
854
        public function getTaxes() : array
855
        {
856
                $taxes = [];
1✔
857

858
                foreach( $this->getProducts() as $product )
1✔
859
                {
860
                        $price = $product->getPrice();
1✔
861

862
                        foreach( $price->getTaxrates() as $name => $taxrate )
1✔
863
                        {
864
                                $price = (clone $price)->setTaxRate( $taxrate );
1✔
865

866
                                if( isset( $taxes[$name][$taxrate] ) ) {
1✔
UNCOV
867
                                        $taxes[$name][$taxrate]->addItem( $price, $product->getQuantity() );
×
868
                                } else {
869
                                        $taxes[$name][$taxrate] = $price->addItem( $price, $product->getQuantity() - 1 );
1✔
870
                                }
871
                        }
872
                }
873

874
                foreach( $this->getServices() as $services )
1✔
875
                {
876
                        foreach( $services as $service )
1✔
877
                        {
878
                                $price = $service->getPrice();
1✔
879

880
                                foreach( $price->getTaxrates() as $name => $taxrate )
1✔
881
                                {
UNCOV
882
                                        $price = (clone $price)->setTaxRate( $taxrate );
×
883

UNCOV
884
                                        if( isset( $taxes[$name][$taxrate] ) ) {
×
UNCOV
885
                                                $taxes[$name][$taxrate]->addItem( $price );
×
886
                                        } else {
UNCOV
887
                                                $taxes[$name][$taxrate] = $price;
×
888
                                        }
889
                                }
890
                        }
891
                }
892

893
                return $taxes;
1✔
894
        }
895

896

897
        /**
898
         * Returns the locales for the basic order item.
899
         *
900
         * @return \Aimeos\MShop\Locale\Item\Iface Object containing information
901
         *  about site, language, country and currency
902
         */
903
        public function locale() : \Aimeos\MShop\Locale\Item\Iface
904
        {
905
                return $this->get( '.locale' );
17✔
906
        }
907

908

909
        /**
910
         * Sets the locales for the basic order item.
911
         *
912
         * @param \Aimeos\MShop\Locale\Item\Iface $locale Object containing information
913
         *  about site, language, country and currency
914
         * @return \Aimeos\MShop\Order\Item\Iface Order base item for chaining method calls
915
         */
916
        public function setLocale( \Aimeos\MShop\Locale\Item\Iface $locale ) : \Aimeos\MShop\Order\Item\Iface
917
        {
918
                $this->notify( 'setLocale.before', $locale );
2✔
919
                $this->set( '.locale', clone $locale );
2✔
920
                $this->notify( 'setLocale.after', $locale );
2✔
921

922
                return parent::setModified();
2✔
923
        }
924

925

926
        /**
927
         * Sets the modified flag of the object.
928
         *
929
         * @return \Aimeos\MShop\Common\Item\Iface Order base item for method chaining
930
         */
931
        public function setModified() : \Aimeos\MShop\Common\Item\Iface
932
        {
933
                $this->recalc = true;
235✔
934
                return parent::setModified();
235✔
935
        }
936

937

938
        /**
939
         * Returns the item values as array.
940
         *
941
         * @param bool True to return private properties, false for public only
942
         * @return array Associative list of item properties and their values
943
         */
944
        public function toArray( bool $private = false ) : array
945
        {
946
                $price = $this->getPrice();
3✔
947
                $list = parent::toArray( $private );
3✔
948

949
                $list['order.currencyid'] = $price->getCurrencyId();
3✔
950
                $list['order.price'] = $price->getValue();
3✔
951
                $list['order.costs'] = $price->getCosts();
3✔
952
                $list['order.rebate'] = $price->getRebate();
3✔
953
                $list['order.taxflag'] = $price->getTaxFlag();
3✔
954
                $list['order.taxvalue'] = $price->getTaxValue();
3✔
955

956
                return $list;
3✔
957
        }
958

959

960
        /**
961
         * Checks if the given delivery status is a valid constant.
962
         *
963
         * @param int $value Delivery status constant defined in \Aimeos\MShop\Order\Item\Base
964
         * @return int Delivery status constant defined in \Aimeos\MShop\Order\Item\Base
965
         * @throws \Aimeos\MShop\Order\Exception If delivery status is invalid
966
         */
967
        protected function checkDeliveryStatus( int $value )
968
        {
UNCOV
969
                if( $value < \Aimeos\MShop\Order\Item\Base::STAT_UNFINISHED || $value > \Aimeos\MShop\Order\Item\Base::STAT_RETURNED ) {
×
UNCOV
970
                        throw new \Aimeos\MShop\Order\Exception( sprintf( 'Order delivery status "%1$s" not within allowed range', $value ) );
×
971
                }
972

UNCOV
973
                return $value;
×
974
        }
975

976

977
        /**
978
         * Checks the given payment status is a valid constant.
979
         *
980
         * @param int $value Payment status constant defined in \Aimeos\MShop\Order\Item\Base
981
         * @return int Payment status constant defined in \Aimeos\MShop\Order\Item\Base
982
         * @throws \Aimeos\MShop\Order\Exception If payment status is invalid
983
         */
984
        protected function checkPaymentStatus( int $value )
985
        {
UNCOV
986
                if( $value < \Aimeos\MShop\Order\Item\Base::PAY_UNFINISHED || $value > \Aimeos\MShop\Order\Item\Base::PAY_RECEIVED ) {
×
UNCOV
987
                        throw new \Aimeos\MShop\Order\Exception( sprintf( 'Order payment status "%1$s" not within allowed range', $value ) );
×
988
                }
989

UNCOV
990
                return $value;
×
991
        }
992

993

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

1005

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

1018
                foreach( $items as $key => $item ) {
7✔
1019
                        $items[$key] = $item->setType( $type );
7✔
1020
                }
1021

1022
                return $items;
7✔
1023
        }
1024

1025

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

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

1043
                        $this->checkPrice( $item->getPrice() );
117✔
1044
                }
1045

1046
                return map( $items );
119✔
1047
        }
1048

1049

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

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

1068
                return $items;
14✔
1069
        }
1070

1071

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

1087
                foreach( $item->getAttributeItems() as $attributeItem )
104✔
1088
                {
1089
                        $key = md5( $attributeItem->getCode() . json_encode( $attributeItem->getValue() ) );
11✔
1090
                        $map[$key] = $attributeItem;
11✔
1091
                        $count++;
11✔
1092
                }
1093

1094
                foreach( $products as $position => $product )
104✔
1095
                {
1096
                        if( $product->compare( $item ) === false ) {
19✔
1097
                                continue;
18✔
1098
                        }
1099

1100
                        $prodAttributes = $product->getAttributeItems();
2✔
1101

1102
                        if( count( $prodAttributes ) !== $count ) {
2✔
UNCOV
1103
                                continue;
×
1104
                        }
1105

1106
                        foreach( $prodAttributes as $attribute )
2✔
1107
                        {
UNCOV
1108
                                $key = md5( $attribute->getCode() . json_encode( $attribute->getValue() ) );
×
1109

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

1115
                        return $position;
2✔
1116
                }
1117

1118
                return null;
104✔
1119
        }
1120
}
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