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

aimeos / aimeos-core / 19bfd2c9-77c1-48e1-a822-6de0edf8768c

11 Aug 2024 11:54AM UTC coverage: 91.806% (+0.004%) from 91.802%
19bfd2c9-77c1-48e1-a822-6de0edf8768c

push

circleci

aimeos
Simplified order coupon manager

13 of 13 new or added lines in 1 file covered. (100.0%)

25 existing lines in 5 files now uncovered.

10453 of 11386 relevant lines covered (91.81%)

59.5 hits per line

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

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

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

11

12
namespace Aimeos\MShop\Order\Item;
13

14

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

28

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

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

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

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

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

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

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

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

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

89

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

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

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

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

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

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

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

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

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

146

147
        // protected is a workaround for serialize problem
148
        protected array $coupons;
149
        protected array $products;
150
        protected array $services = [];
151
        protected array $addresses = [];
152

153

154
        /**
155
         * Initializes the basket object
156
         *
157
         * @param \Aimeos\MShop\Price\Item\Iface $price Default price of the basket (usually 0.00)
158
         * @param \Aimeos\MShop\Locale\Item\Iface $locale Locale item containing the site, language and currency
159
         * @param array $values Associative list of key/value pairs containing, e.g. the order or user ID
160
         * @param array $products List of ordered products implementing \Aimeos\MShop\Order\Item\Product\Iface
161
         * @param array $addresses List of order addresses implementing \Aimeos\MShop\Order\Item\Address\Iface
162
         * @param array $services List of order services implementing \Aimeos\MShop\Order\Item\Service\Iface
163
         * @param array $coupons Associative list of coupon codes as keys and ordered products implementing \Aimeos\MShop\Order\Item\Product\Iface as values
164
         */
165
        public function __construct( \Aimeos\MShop\Price\Item\Iface $price, \Aimeos\MShop\Locale\Item\Iface $locale,
166
                array $values = [], array $products = [], array $addresses = [], array $services = [], array $coupons = [] )
167
        {
168
                map( $addresses )->implements( \Aimeos\MShop\Order\Item\Address\Iface::class, true );
544✔
169
                map( $products )->implements( \Aimeos\MShop\Order\Item\Product\Iface::class, true );
544✔
170
                map( $services )->implements( \Aimeos\MShop\Order\Item\Service\Iface::class, true );
544✔
171

172
                foreach( $coupons as $couponProducts ) {
544✔
173
                        map( $couponProducts )->implements( \Aimeos\MShop\Order\Item\Product\Iface::class, true );
6✔
174
                }
175

176
                parent::__construct( 'order.', $values );
544✔
177

178
                $this->coupons = $coupons;
544✔
179
                $this->products = $products;
544✔
180

181
                foreach( $addresses as $address ) {
544✔
182
                        $this->addresses[$address->getType()][] = $address;
32✔
183
                }
184

185
                foreach( $services as $service ) {
544✔
186
                        $this->services[$service->getType()][] = $service;
33✔
187
                }
188
        }
189

190

191
        /**
192
         * Specifies the data which should be serialized to JSON by json_encode().
193
         *
194
         * @return array<string,mixed> Data to serialize to JSON
195
         */
196
        #[\ReturnTypeWillChange]
197
        public function jsonSerialize()
198
        {
199
                return parent::jsonSerialize() + [
1✔
200
                        'coupons' => $this->coupons,
1✔
201
                        'products' => $this->products,
1✔
202
                        'services' => $this->services,
1✔
203
                        'addresses' => $this->addresses,
1✔
204
                ];
1✔
205
        }
206

207

208
        /**
209
         * Prepares the object for serialization.
210
         *
211
         * @return array List of properties that should be serialized
212
         */
213
        public function __sleep() : array
214
        {
215
                /*
216
                 * Workaround because database connections can't be serialized
217
                 * Listeners will be reattached on wakeup by the order base manager
218
                 */
219
                $this->off();
2✔
220

221
                return array_keys( get_object_vars( $this ) );
2✔
222
        }
223

224

225
        /**
226
         * Returns the ID of the items
227
         *
228
         * @return string ID of the item or null
229
         */
230
        public function __toString() : string
231
        {
UNCOV
232
                return (string) $this->getId();
×
233
        }
234

235

236
        /**
237
         * Tests if all necessary items are available to create the order.
238
         *
239
         * @param array $what Type of data
240
         * @return \Aimeos\MShop\Order\Item\Iface Order base item for method chaining
241
         * @throws \Aimeos\MShop\Order\Exception if there are no products in the basket
242
         */
243
        public function check( array $what = ['order/address', 'order/coupon', 'order/product', 'order/service'] ) : \Aimeos\MShop\Order\Item\Iface
244
        {
245
                $this->notify( 'check.before', $what );
3✔
246

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

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

253
                return $this;
1✔
254
        }
255

256

257
        /**
258
         * Notifies listeners before the basket becomes an order.
259
         *
260
         * @return \Aimeos\MShop\Order\Item\Iface Order base item for chaining method calls
261
         */
262
        public function finish() : \Aimeos\MShop\Order\Item\Iface
263
        {
UNCOV
264
                $this->notify( 'setOrder.before' );
×
265
                return $this;
×
266
        }
267

268

269
        /**
270
         * Adds the address of the given type to the basket
271
         *
272
         * @param \Aimeos\MShop\Order\Item\Address\Iface $address Order address item for the given type
273
         * @param string $type Address type, usually "billing" or "delivery"
274
         * @param int|null $position Position of the address in the list
275
         * @return \Aimeos\MShop\Order\Item\Iface Order base item for method chaining
276
         */
277
        public function addAddress( \Aimeos\MShop\Order\Item\Address\Iface $address, string $type, int $position = null ) : \Aimeos\MShop\Order\Item\Iface
278
        {
279
                $address = $this->notify( 'addAddress.before', $address );
52✔
280

281
                $address = clone $address;
52✔
282
                $address = $address->setType( $type );
52✔
283

284
                if( $position !== null ) {
52✔
285
                        $this->addresses[$type][$position] = $address;
1✔
286
                } else {
287
                        $this->addresses[$type][] = $address;
52✔
288
                }
289

290
                $this->setModified();
52✔
291

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

294
                return $this;
52✔
295
        }
296

297

298
        /**
299
         * Deletes an order address from the basket
300
         *
301
         * @param string $type Address type defined in \Aimeos\MShop\Order\Item\Address\Base
302
         * @param int|null $position Position of the address in the list
303
         * @return \Aimeos\MShop\Order\Item\Iface Order base item for method chaining
304
         */
305
        public function deleteAddress( string $type, int $position = null ) : \Aimeos\MShop\Order\Item\Iface
306
        {
307
                if( $position === null && isset( $this->addresses[$type] ) || isset( $this->addresses[$type][$position] ) )
4✔
308
                {
309
                        $old = ( isset( $this->addresses[$type][$position] ) ? $this->addresses[$type][$position] : $this->addresses[$type] );
3✔
310
                        $old = $this->notify( 'deleteAddress.before', $old );
3✔
311

312
                        if( $position !== null ) {
3✔
313
                                unset( $this->addresses[$type][$position] );
1✔
314
                        } else {
315
                                unset( $this->addresses[$type] );
2✔
316
                        }
317

318
                        $this->setModified();
3✔
319

320
                        $this->notify( 'deleteAddress.after', $old );
3✔
321
                }
322

323
                return $this;
4✔
324
        }
325

326

327
        /**
328
         * Returns the order address depending on the given type
329
         *
330
         * @param string $type Address type, usually "billing" or "delivery"
331
         * @param int|null $position Address position in list of addresses
332
         * @return \Aimeos\MShop\Order\Item\Address\Iface[]|\Aimeos\MShop\Order\Item\Address\Iface Order address item or list of
333
         */
334
        public function getAddress( string $type, int $position = null )
335
        {
336
                if( $position !== null )
59✔
337
                {
338
                        if( isset( $this->addresses[$type][$position] ) ) {
4✔
339
                                return $this->addresses[$type][$position];
3✔
340
                        }
341

342
                        throw new \Aimeos\MShop\Order\Exception( sprintf( 'Address not available' ) );
1✔
343
                }
344

345
                return ( isset( $this->addresses[$type] ) ? $this->addresses[$type] : [] );
55✔
346
        }
347

348

349
        /**
350
         * Returns all addresses that are part of the basket
351
         *
352
         * @return \Aimeos\Map Associative list of address items implementing
353
         *  \Aimeos\MShop\Order\Item\Address\Iface with "billing" or "delivery" as key
354
         */
355
        public function getAddresses() : \Aimeos\Map
356
        {
357
                return map( $this->addresses );
29✔
358
        }
359

360

361
        /**
362
         * Replaces all addresses in the current basket with the new ones
363
         *
364
         * @param \Aimeos\Map|array $map Associative list of order addresses as returned by getAddresses()
365
         * @return \Aimeos\MShop\Order\Item\Iface Order base item for method chaining
366
         */
367
        public function setAddresses( iterable $map ) : \Aimeos\MShop\Order\Item\Iface
368
        {
369
                $map = $this->notify( 'setAddresses.before', $map );
7✔
370

371
                foreach( $map as $type => $items ) {
7✔
372
                        $this->checkAddresses( $items, $type );
7✔
373
                }
374

375
                $old = $this->addresses;
7✔
376
                $this->addresses = is_map( $map ) ? $map->toArray() : $map;
7✔
377
                $this->setModified();
7✔
378

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

381
                return $this;
7✔
382
        }
383

384

385
        /**
386
         * Adds a coupon code and the given product item to the basket
387
         *
388
         * @param string $code Coupon code
389
         * @return \Aimeos\MShop\Order\Item\Iface Order base item for method chaining
390
         */
391
        public function addCoupon( string $code ) : \Aimeos\MShop\Order\Item\Iface
392
        {
393
                if( !isset( $this->coupons[$code] ) )
3✔
394
                {
395
                        $code = $this->notify( 'addCoupon.before', $code );
3✔
396

397
                        $this->coupons[$code] = [];
3✔
398
                        $this->setModified();
3✔
399

400
                        $this->notify( 'addCoupon.after', $code );
3✔
401
                }
402

403
                return $this;
3✔
404
        }
405

406

407
        /**
408
         * Removes a coupon and the related product items from the basket
409
         *
410
         * @param string $code Coupon code
411
         * @return \Aimeos\MShop\Order\Item\Iface Order base item for method chaining
412
         */
413
        public function deleteCoupon( string $code ) : \Aimeos\MShop\Order\Item\Iface
414
        {
415
                if( isset( $this->coupons[$code] ) )
1✔
416
                {
417
                        $old = [$code => $this->coupons[$code]];
1✔
418
                        $old = $this->notify( 'deleteCoupon.before', $old );
1✔
419

420
                        foreach( $this->coupons[$code] as $product )
1✔
421
                        {
422
                                if( ( $key = array_search( $product, $this->products, true ) ) !== false ) {
1✔
423
                                        unset( $this->products[$key] );
1✔
424
                                }
425
                        }
426

427
                        unset( $this->coupons[$code] );
1✔
428
                        $this->setModified();
1✔
429

430
                        $this->notify( 'deleteCoupon.after', $old );
1✔
431
                }
432

433
                return $this;
1✔
434
        }
435

436

437
        /**
438
         * Returns the available coupon codes and the lists of affected product items
439
         *
440
         * @return \Aimeos\Map Associative array of codes and lists of product items
441
         *  implementing \Aimeos\MShop\Order\Product\Iface
442
         */
443
        public function getCoupons() : \Aimeos\Map
444
        {
445
                return map( $this->coupons );
74✔
446
        }
447

448

449
        /**
450
         * Sets a coupon code and the given product items in the basket.
451
         *
452
         * @param string $code Coupon code
453
         * @param \Aimeos\MShop\Order\Item\Product\Iface[] $products List of coupon products
454
         * @return \Aimeos\MShop\Order\Item\Iface Order base item for method chaining
455
         */
456
        public function setCoupon( string $code, iterable $products = [] ) : \Aimeos\MShop\Order\Item\Iface
457
        {
458
                $new = $this->notify( 'setCoupon.before', [$code => $products] );
16✔
459

460
                $products = $this->checkProducts( map( $new )->first( [] ) );
16✔
461

462
                if( isset( $this->coupons[$code] ) )
16✔
463
                {
464
                        foreach( $this->coupons[$code] as $product )
8✔
465
                        {
UNCOV
466
                                if( ( $key = array_search( $product, $this->products, true ) ) !== false ) {
×
467
                                        unset( $this->products[$key] );
×
468
                                }
469
                        }
470
                }
471

472
                foreach( $products as $product ) {
16✔
473
                        $this->products[] = $product;
14✔
474
                }
475

476
                $old = isset( $this->coupons[$code] ) ? [$code => $this->coupons[$code]] : [];
16✔
477
                $this->coupons[$code] = is_map( $products ) ? $products->toArray() : $products;
16✔
478
                $this->setModified();
16✔
479

480
                $this->notify( 'setCoupon.after', $old );
16✔
481

482
                return $this;
16✔
483
        }
484

485

486
        /**
487
         * Replaces all coupons in the current basket with the new ones
488
         *
489
         * @param iterable $map Associative list of order coupons as returned by getCoupons()
490
         * @return \Aimeos\MShop\Order\Item\Iface Order base item for method chaining
491
         */
492
        public function setCoupons( iterable $map ) : \Aimeos\MShop\Order\Item\Iface
493
        {
494
                $map = $this->notify( 'setCoupons.before', $map );
3✔
495

496
                foreach( $map as $code => $products ) {
3✔
497
                        $map[$code] = $this->checkProducts( $products );
3✔
498
                }
499

500
                foreach( $this->coupons as $code => $products )
3✔
501
                {
UNCOV
502
                        foreach( $products as $product )
×
503
                        {
UNCOV
504
                                if( ( $key = array_search( $product, $this->products, true ) ) !== false ) {
×
505
                                        unset( $this->products[$key] );
×
506
                                }
507
                        }
508
                }
509

510
                foreach( $map as $code => $products )
3✔
511
                {
512
                        foreach( $products as $product ) {
3✔
513
                                $this->products[] = $product;
3✔
514
                        }
515
                }
516

517
                $old = $this->coupons;
3✔
518
                $this->coupons = is_map( $map ) ? $map->toArray() : $map;
3✔
519
                $this->setModified();
3✔
520

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

523
                return $this;
3✔
524
        }
525

526

527
        /**
528
         * Adds an order product item to the basket
529
         * If a similar item is found, only the quantity is increased.
530
         *
531
         * @param \Aimeos\MShop\Order\Item\Product\Iface $item Order product item to be added
532
         * @param int|null $position position of the new order product item
533
         * @return \Aimeos\MShop\Order\Item\Iface Order base item for method chaining
534
         */
535
        public function addProduct( \Aimeos\MShop\Order\Item\Product\Iface $item, int $position = null ) : \Aimeos\MShop\Order\Item\Iface
536
        {
537
                $item = $this->notify( 'addProduct.before', $item );
106✔
538

539
                $this->checkProducts( [$item] );
106✔
540

541
                if( $position !== null ) {
106✔
542
                        $this->products[$position] = $item;
8✔
543
                } elseif( ( $pos = $this->getSameProduct( $item, $this->products ) ) !== null ) {
104✔
544
                        $item = $this->products[$pos]->setQuantity( $this->products[$pos]->getQuantity() + $item->getQuantity() );
2✔
545
                } else {
546
                        $this->products[] = $item;
104✔
547
                }
548

549
                ksort( $this->products );
106✔
550
                $this->setModified();
106✔
551

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

554
                return $this;
106✔
555
        }
556

557

558
        /**
559
         * Deletes an order product item from the basket
560
         *
561
         * @param int $position Position of the order product item
562
         * @return \Aimeos\MShop\Order\Item\Iface Order base item for method chaining
563
         */
564
        public function deleteProduct( int $position ) : \Aimeos\MShop\Order\Item\Iface
565
        {
566
                if( isset( $this->products[$position] ) )
4✔
567
                {
568
                        $old = $this->products[$position];
4✔
569
                        $old = $this->notify( 'deleteProduct.before', $old );
4✔
570

571
                        unset( $this->products[$position] );
4✔
572
                        $this->setModified();
4✔
573

574
                        $this->notify( 'deleteProduct.after', $old );
4✔
575
                }
576

577
                return $this;
4✔
578
        }
579

580

581
        /**
582
         * Returns the product item of an basket specified by its key
583
         *
584
         * @param int $key Key returned by getProducts() identifying the requested product
585
         * @return \Aimeos\MShop\Order\Item\Product\Iface Product item of an order
586
         */
587
        public function getProduct( int $key ) : \Aimeos\MShop\Order\Item\Product\Iface
588
        {
589
                if( !isset( $this->products[$key] ) ) {
13✔
UNCOV
590
                        throw new \Aimeos\MShop\Order\Exception( sprintf( 'Product not available' ) );
×
591
                }
592

593
                return $this->products[$key];
13✔
594
        }
595

596

597
        /**
598
         * Returns the product items that are or should be part of a basket
599
         *
600
         * @return \Aimeos\Map List of order product items implementing \Aimeos\MShop\Order\Item\Product\Iface
601
         */
602
        public function getProducts() : \Aimeos\Map
603
        {
604
                return map( $this->products );
141✔
605
        }
606

607

608
        /**
609
         * Replaces all products in the current basket with the new ones
610
         *
611
         * @param \Aimeos\MShop\Order\Item\Product\Iface[] $map Associative list of ordered products as returned by getProducts()
612
         * @return \Aimeos\MShop\Order\Item\Iface Order base item for method chaining
613
         */
614
        public function setProducts( iterable $map ) : \Aimeos\MShop\Order\Item\Iface
615
        {
616
                $map = $this->notify( 'setProducts.before', $map );
8✔
617

618
                $this->checkProducts( $map );
8✔
619

620
                $old = $this->products;
8✔
621
                $this->products = is_map( $map ) ? $map->toArray() : $map;
8✔
622
                $this->setModified();
8✔
623

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

626
                return $this;
8✔
627
        }
628

629

630
        /**
631
         * Adds an order service to the basket
632
         *
633
         * @param \Aimeos\MShop\Order\Item\Service\Iface $service Order service item for the given domain
634
         * @param string $type Service type constant from \Aimeos\MShop\Order\Item\Service\Base
635
         * @param int|null $position Position of the service in the list to overwrite
636
         * @return \Aimeos\MShop\Order\Item\Iface Order base item for method chaining
637
         */
638
        public function addService( \Aimeos\MShop\Order\Item\Service\Iface $service, string $type, int $position = null ) : \Aimeos\MShop\Order\Item\Iface
639
        {
640
                $service = $this->notify( 'addService.before', $service );
23✔
641

642
                $this->checkPrice( $service->getPrice() );
23✔
643

644
                $service = clone $service;
23✔
645
                $service = $service->setType( $type );
23✔
646

647
                if( $position !== null ) {
23✔
648
                        $this->services[$type][$position] = $service;
1✔
649
                } else {
650
                        $this->services[$type][] = $service;
23✔
651
                }
652

653
                $this->setModified();
23✔
654

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

657
                return $this;
23✔
658
        }
659

660

661
        /**
662
         * Deletes an order service from the basket
663
         *
664
         * @param string $type Service type constant from \Aimeos\MShop\Order\Item\Service\Base
665
         * @param int|null $position Position of the service in the list to delete
666
         * @return \Aimeos\MShop\Order\Item\Iface Order base item for method chaining
667
         */
668
        public function deleteService( string $type, int $position = null ) : \Aimeos\MShop\Order\Item\Iface
669
        {
670
                if( $position === null && isset( $this->services[$type] ) || isset( $this->services[$type][$position] ) )
4✔
671
                {
672
                        $old = ( isset( $this->services[$type][$position] ) ? $this->services[$type][$position] : $this->services[$type] );
3✔
673
                        $old = $this->notify( 'deleteService.before', $old );
3✔
674

675
                        if( $position !== null ) {
3✔
676
                                unset( $this->services[$type][$position] );
1✔
677
                        } else {
678
                                unset( $this->services[$type] );
2✔
679
                        }
680

681
                        $this->setModified();
3✔
682

683
                        $this->notify( 'deleteService.after', $old );
3✔
684
                }
685

686
                return $this;
4✔
687
        }
688

689

690
        /**
691
         * Returns the order services depending on the given type
692
         *
693
         * @param string $type Service type constant from \Aimeos\MShop\Order\Item\Service\Base
694
         * @param int|null $position Position of the service in the list to retrieve
695
         * @return \Aimeos\MShop\Order\Item\Service\Iface[]|\Aimeos\MShop\Order\Item\Service\Iface
696
         *         Order service item or list of items for the requested type
697
         * @throws \Aimeos\MShop\Order\Exception If no service for the given type and position is found
698
         */
699
        public function getService( string $type, int $position = null )
700
        {
701
                if( $position !== null )
46✔
702
                {
703
                        if( isset( $this->services[$type][$position] ) ) {
20✔
704
                                return $this->services[$type][$position];
19✔
705
                        }
706

707
                        throw new \Aimeos\MShop\Order\Exception( sprintf( 'Service not available' ) );
1✔
708
                }
709

710
                return ( isset( $this->services[$type] ) ? $this->services[$type] : [] );
36✔
711
        }
712

713

714
        /**
715
         * Returns all services that are part of the basket
716
         *
717
         * @return \Aimeos\Map Associative list of service types ("delivery" or "payment") as keys and list of
718
         *        service items implementing \Aimeos\MShop\Order\Service\Iface as values
719
         */
720
        public function getServices() : \Aimeos\Map
721
        {
722
                return map( $this->services );
92✔
723
        }
724

725

726
        /**
727
         * Replaces all services in the current basket with the new ones
728
         *
729
         * @param \Aimeos\MShop\Order\Item\Service\Iface[] $map Associative list of order services as returned by getServices()
730
         * @return \Aimeos\MShop\Order\Item\Iface Order base item for method chaining
731
         */
732
        public function setServices( iterable $map ) : \Aimeos\MShop\Order\Item\Iface
733
        {
734
                $map = $this->notify( 'setServices.before', $map );
14✔
735

736
                foreach( $map as $type => $services ) {
14✔
737
                        $map[$type] = $this->checkServices( $services, $type );
14✔
738
                }
739

740
                $old = $this->services;
14✔
741
                $this->services = is_map( $map ) ? $map->toArray() : $map;
14✔
742
                $this->setModified();
14✔
743

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

746
                return $this;
14✔
747
        }
748

749

750
        /**
751
         * Returns the service costs
752
         *
753
         * @param string $type Service type like "delivery" or "payment"
754
         * @return float Service costs value
755
         */
756
        public function getCosts( string $type = 'delivery' ) : float
757
        {
758
                $costs = 0;
1✔
759

760
                if( $type === 'delivery' )
1✔
761
                {
762
                        foreach( $this->getProducts() as $product ) {
1✔
763
                                $costs += $product->getPrice()->getCosts() * $product->getQuantity();
1✔
764
                        }
765
                }
766

767
                foreach( $this->getService( $type ) as $service ) {
1✔
768
                        $costs += $service->getPrice()->getCosts();
1✔
769
                }
770

771
                return $costs;
1✔
772
        }
773

774

775
        /**
776
         * Returns a list of tax names and values
777
         *
778
         * @return array Associative list of tax names as key and price items as value
779
         */
780
        public function getTaxes() : array
781
        {
782
                $taxes = [];
1✔
783

784
                foreach( $this->getProducts() as $product )
1✔
785
                {
786
                        $price = $product->getPrice();
1✔
787

788
                        foreach( $price->getTaxrates() as $name => $taxrate )
1✔
789
                        {
790
                                $price = (clone $price)->setTaxRate( $taxrate );
1✔
791

792
                                if( isset( $taxes[$name][$taxrate] ) ) {
1✔
UNCOV
793
                                        $taxes[$name][$taxrate]->addItem( $price, $product->getQuantity() );
×
794
                                } else {
795
                                        $taxes[$name][$taxrate] = $price->addItem( $price, $product->getQuantity() - 1 );
1✔
796
                                }
797
                        }
798
                }
799

800
                foreach( $this->getServices() as $services )
1✔
801
                {
802
                        foreach( $services as $service )
1✔
803
                        {
804
                                $price = $service->getPrice();
1✔
805

806
                                foreach( $price->getTaxrates() as $name => $taxrate )
1✔
807
                                {
UNCOV
808
                                        $price = (clone $price)->setTaxRate( $taxrate );
×
809

UNCOV
810
                                        if( isset( $taxes[$name][$taxrate] ) ) {
×
811
                                                $taxes[$name][$taxrate]->addItem( $price );
×
812
                                        } else {
UNCOV
813
                                                $taxes[$name][$taxrate] = $price;
×
814
                                        }
815
                                }
816
                        }
817
                }
818

819
                return $taxes;
1✔
820
        }
821

822

823
        /**
824
         * Checks if the price uses the same currency as the price in the basket.
825
         *
826
         * @param \Aimeos\MShop\Price\Item\Iface $item Price item
827
         */
828
        protected function checkPrice( \Aimeos\MShop\Price\Item\Iface $item )
829
        {
830
                $price = clone $this->getPrice();
143✔
831
                $price->addItem( $item );
143✔
832
        }
833

834

835
        /**
836
         * Checks if the given delivery status is a valid constant.
837
         *
838
         * @param int $value Delivery status constant defined in \Aimeos\MShop\Order\Item\Base
839
         * @return int Delivery status constant defined in \Aimeos\MShop\Order\Item\Base
840
         * @throws \Aimeos\MShop\Order\Exception If delivery status is invalid
841
         */
842
        protected function checkDeliveryStatus( int $value )
843
        {
UNCOV
844
                if( $value < \Aimeos\MShop\Order\Item\Base::STAT_UNFINISHED || $value > \Aimeos\MShop\Order\Item\Base::STAT_RETURNED ) {
×
845
                        throw new \Aimeos\MShop\Order\Exception( sprintf( 'Order delivery status "%1$s" not within allowed range', $value ) );
×
846
                }
847

UNCOV
848
                return $value;
×
849
        }
850

851

852
        /**
853
         * Checks the given payment status is a valid constant.
854
         *
855
         * @param int $value Payment status constant defined in \Aimeos\MShop\Order\Item\Base
856
         * @return int Payment status constant defined in \Aimeos\MShop\Order\Item\Base
857
         * @throws \Aimeos\MShop\Order\Exception If payment status is invalid
858
         */
859
        protected function checkPaymentStatus( int $value )
860
        {
UNCOV
861
                if( $value < \Aimeos\MShop\Order\Item\Base::PAY_UNFINISHED || $value > \Aimeos\MShop\Order\Item\Base::PAY_RECEIVED ) {
×
862
                        throw new \Aimeos\MShop\Order\Exception( sprintf( 'Order payment status "%1$s" not within allowed range', $value ) );
×
863
                }
864

UNCOV
865
                return $value;
×
866
        }
867

868

869
        /**
870
         * Checks if all order addresses are valid
871
         *
872
         * @param \Aimeos\MShop\Order\Item\Address\Iface[] $items Order address items
873
         * @param string $type Address type constant from \Aimeos\MShop\Order\Item\Address\Base
874
         * @return \Aimeos\MShop\Order\Item\Address\Iface[] List of checked items
875
         * @throws \Aimeos\MShop\Exception If one of the order addresses is invalid
876
         */
877
        protected function checkAddresses( iterable $items, string $type ) : iterable
878
        {
879
                map( $items )->implements( \Aimeos\MShop\Order\Item\Address\Iface::class, true );
7✔
880

881
                foreach( $items as $key => $item ) {
7✔
882
                        $items[$key] = $item->setType( $type );
7✔
883
                }
884

885
                return $items;
7✔
886
        }
887

888

889
        /**
890
         * Checks if all order products are valid
891
         *
892
         * @param \Aimeos\MShop\Order\Item\Product\Iface[] $items Order product items
893
         * @return \Aimeos\MShop\Order\Item\Product\Iface[] List of checked items
894
         * @throws \Aimeos\MShop\Exception If one of the order products is invalid
895
         */
896
        protected function checkProducts( iterable $items ) : \Aimeos\Map
897
        {
898
                map( $items )->implements( \Aimeos\MShop\Order\Item\Product\Iface::class, true );
118✔
899

900
                foreach( $items as $key => $item )
118✔
901
                {
902
                        if( $item->getProductCode() === '' ) {
116✔
UNCOV
903
                                throw new \Aimeos\MShop\Order\Exception( sprintf( 'Product does not contain the SKU code' ) );
×
904
                        }
905

906
                        $this->checkPrice( $item->getPrice() );
116✔
907
                }
908

909
                return map( $items );
118✔
910
        }
911

912

913
        /**
914
         * Checks if all order services are valid
915
         *
916
         * @param \Aimeos\MShop\Order\Item\Service\Iface[] $items Order service items
917
         * @param string $type Service type constant from \Aimeos\MShop\Order\Item\Service\Base
918
         * @return \Aimeos\MShop\Order\Item\Service\Iface[] List of checked items
919
         * @throws \Aimeos\MShop\Exception If one of the order services is invalid
920
         */
921
        protected function checkServices( iterable $items, string $type ) : iterable
922
        {
923
                map( $items )->implements( \Aimeos\MShop\Order\Item\Service\Iface::class, true );
14✔
924

925
                foreach( $items as $key => $item )
14✔
926
                {
927
                        $this->checkPrice( $item->getPrice() );
12✔
928
                        $items[$key] = $item->setType( $type );
12✔
929
                }
930

931
                return $items;
14✔
932
        }
933

934

935
        /**
936
         * Tests if the given product is similar to an existing one.
937
         * Similarity is described by the equality of properties so the quantity of
938
         * the existing product can be updated.
939
         *
940
         * @param \Aimeos\MShop\Order\Item\Product\Iface $item Order product item
941
         * @param \Aimeos\MShop\Order\Item\Product\Iface[] $products List of order product items to check against
942
         * @return int|null Positon of the same product in the product list of false if product is unique
943
         * @throws \Aimeos\MShop\Order\Exception If no similar item was found
944
         */
945
        protected function getSameProduct( \Aimeos\MShop\Order\Item\Product\Iface $item, iterable $products ) : ?int
946
        {
947
                $map = [];
104✔
948
                $count = 0;
104✔
949

950
                foreach( $item->getAttributeItems() as $attributeItem )
104✔
951
                {
952
                        $key = md5( $attributeItem->getCode() . json_encode( $attributeItem->getValue() ) );
11✔
953
                        $map[$key] = $attributeItem;
11✔
954
                        $count++;
11✔
955
                }
956

957
                foreach( $products as $position => $product )
104✔
958
                {
959
                        if( $product->compare( $item ) === false ) {
19✔
960
                                continue;
18✔
961
                        }
962

963
                        $prodAttributes = $product->getAttributeItems();
2✔
964

965
                        if( count( $prodAttributes ) !== $count ) {
2✔
UNCOV
966
                                continue;
×
967
                        }
968

969
                        foreach( $prodAttributes as $attribute )
2✔
970
                        {
UNCOV
971
                                $key = md5( $attribute->getCode() . json_encode( $attribute->getValue() ) );
×
972

UNCOV
973
                                if( isset( $map[$key] ) === false || $map[$key]->getQuantity() != $attribute->getQuantity() ) {
×
974
                                        continue 2; // jump to outer loop
×
975
                                }
976
                        }
977

978
                        return $position;
2✔
979
                }
980

981
                return null;
104✔
982
        }
983
}
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