Passed
Push — master ( 64e7d7...5c5569 )
by Aimeos
04:35
created

Base::getService()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 12
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 5
c 0
b 0
f 0
dl 0
loc 12
rs 10
cc 4
nc 4
nop 2
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 $statuses = [];
155
	protected array $addresses = [];
156
157
158
	/**
159
	 * Initializes the order object
160
	 *
161
	 * @param string $prefix Prefix for the keys in the associative array
162
	 * @param array $values Associative list of key/value pairs containing, e.g. the order or user ID
163
	 */
164
	public function __construct( string $prefix, array $values = [] )
165
	{
166
		parent::__construct( $prefix, $values );
167
168
		$this->customer = $values['.customer'] ?? null;
169
		$this->locale = $values['.locale'];
170
		$this->price = $values['.price'];
171
172
		$products = $this->get( '.products', [] );
173
174
		foreach( $this->get( '.coupons', [] ) as $coupon )
175
		{
176
			if( !isset( $this->coupons[$coupon->getCode()] ) ) {
177
				$this->coupons[$coupon->getCode()] = [];
178
			}
179
180
			if( isset( $products[$coupon->getProductId()] ) ) {
181
				$this->coupons[$coupon->getCode()][] = $products[$coupon->getProductId()];
182
			}
183
		}
184
185
		foreach( $this->get( '.products', [] ) as $product ) {
186
			$this->products[$product->getPosition()] = $product;
187
		}
188
189
		foreach( $this->get( '.addresses', [] ) as $address ) {
190
			$this->addresses[$address->getType()][] = $address;
191
		}
192
193
		foreach( $this->get( '.services', [] ) as $service ) {
194
			$this->services[$service->getType()][] = $service;
195
		}
196
197
		foreach( $this->get( '.statuses', [] ) as $status ) {
198
			$this->statuses[$status->getType()][$status->getValue()] = $status;
199
		}
200
	}
201
202
203
	/**
204
	 * Clones internal objects of the order item.
205
	 */
206
	public function __clone()
207
	{
208
		parent::__clone();
209
210
		$this->price = clone $this->price;
211
		$this->locale = clone $this->locale;
212
	}
213
214
215
	/**
216
	 * Specifies the data which should be serialized to JSON by json_encode().
217
	 *
218
	 * @return array<string,mixed> Data to serialize to JSON
219
	 */
220
	#[\ReturnTypeWillChange]
221
	public function jsonSerialize()
222
	{
223
		return parent::jsonSerialize() + [
224
			'addresses' => $this->addresses,
225
			'products' => $this->products,
226
			'services' => $this->services,
227
			'coupons' => $this->coupons,
228
			'customer' => $this->customer,
229
			'locale' => $this->locale,
230
			'price' => $this->price,
231
		];
232
	}
233
234
235
	/**
236
	 * Prepares the object for serialization.
237
	 *
238
	 * @return array List of properties that should be serialized
239
	 */
240
	public function __sleep() : array
241
	{
242
		/*
243
		 * Workaround because database connections can't be serialized
244
		 * Listeners will be reattached on wakeup by the order base manager
245
		 */
246
		$this->off();
247
248
		return array_keys( get_object_vars( $this ) );
249
	}
250
251
252
	/**
253
	 * Returns the ID of the items
254
	 *
255
	 * @return string ID of the item or null
256
	 */
257
	public function __toString() : string
258
	{
259
		return (string) $this->getId();
260
	}
261
262
263
	/**
264
	 * Tests if all necessary items are available to create the order.
265
	 *
266
	 * @param array $what Type of data
267
	 * @return \Aimeos\MShop\Order\Item\Iface Order base item for method chaining
268
	 * @throws \Aimeos\MShop\Order\Exception if there are no products in the basket
269
	 */
270
	public function check( array $what = ['order/address', 'order/coupon', 'order/product', 'order/service'] ) : \Aimeos\MShop\Order\Item\Iface
271
	{
272
		$this->notify( 'check.before', $what );
273
274
		if( in_array( 'order/product', $what ) && ( count( $this->getProducts() ) < 1 ) ) {
275
			throw new \Aimeos\MShop\Order\Exception( sprintf( 'Basket empty' ) );
276
		}
277
278
		$this->notify( 'check.after', $what );
279
280
		return $this;
281
	}
282
283
284
	/**
285
	 * Notifies listeners before the basket becomes an order.
286
	 *
287
	 * @return \Aimeos\MShop\Order\Item\Iface Order base item for chaining method calls
288
	 */
289
	public function finish() : \Aimeos\MShop\Order\Item\Iface
290
	{
291
		$this->notify( 'setOrder.before' );
292
		return $this;
293
	}
294
295
296
	/**
297
	 * Adds the address of the given type to the basket
298
	 *
299
	 * @param \Aimeos\MShop\Order\Item\Address\Iface $address Order address item for the given type
300
	 * @param string $type Address type, usually "billing" or "delivery"
301
	 * @param int|null $position Position of the address in the list
302
	 * @return \Aimeos\MShop\Order\Item\Iface Order base item for method chaining
303
	 */
304
	public function addAddress( \Aimeos\MShop\Order\Item\Address\Iface $address, string $type, int $position = null ) : \Aimeos\MShop\Order\Item\Iface
305
	{
306
		$address = $this->notify( 'addAddress.before', $address );
307
308
		$address = clone $address;
309
		$address = $address->setType( $type );
310
311
		if( $position !== null ) {
312
			$this->addresses[$type][$position] = $address;
313
		} else {
314
			$this->addresses[$type][] = $address;
315
		}
316
317
		$this->setModified();
318
319
		$this->notify( 'addAddress.after', $address );
320
321
		return $this;
322
	}
323
324
325
	/**
326
	 * Deletes an order address from the basket
327
	 *
328
	 * @param string $type Address type defined in \Aimeos\MShop\Order\Item\Address\Base
329
	 * @param int|null $position Position of the address in the list
330
	 * @return \Aimeos\MShop\Order\Item\Iface Order base item for method chaining
331
	 */
332
	public function deleteAddress( string $type, int $position = null ) : \Aimeos\MShop\Order\Item\Iface
333
	{
334
		if( $position === null && isset( $this->addresses[$type] ) || isset( $this->addresses[$type][$position] ) )
0 ignored issues
show
introduced by
Consider adding parentheses for clarity. Current Interpretation: ($position === null && IssetNode) || IssetNode, Probably Intended Meaning: $position === null && (IssetNode || IssetNode)
Loading history...
335
		{
336
			$old = ( isset( $this->addresses[$type][$position] ) ? $this->addresses[$type][$position] : $this->addresses[$type] );
337
			$old = $this->notify( 'deleteAddress.before', $old );
338
339
			if( $position !== null ) {
340
				unset( $this->addresses[$type][$position] );
341
			} else {
342
				unset( $this->addresses[$type] );
343
			}
344
345
			$this->setModified();
346
347
			$this->notify( 'deleteAddress.after', $old );
348
		}
349
350
		return $this;
351
	}
352
353
354
	/**
355
	 * Returns the order address depending on the given type
356
	 *
357
	 * @param string $type Address type, usually "billing" or "delivery"
358
	 * @param int|null $position Address position in list of addresses
359
	 * @return \Aimeos\MShop\Order\Item\Address\Iface[]|\Aimeos\MShop\Order\Item\Address\Iface Order address item or list of
360
	 */
361
	public function getAddress( string $type, int $position = null )
362
	{
363
		if( $position !== null )
364
		{
365
			if( isset( $this->addresses[$type][$position] ) ) {
366
				return $this->addresses[$type][$position];
367
			}
368
369
			throw new \Aimeos\MShop\Order\Exception( sprintf( 'Address not available' ) );
370
		}
371
372
		return ( isset( $this->addresses[$type] ) ? $this->addresses[$type] : [] );
373
	}
374
375
376
	/**
377
	 * Returns all addresses that are part of the basket
378
	 *
379
	 * @return \Aimeos\Map Associative list of address items implementing
380
	 *  \Aimeos\MShop\Order\Item\Address\Iface with "billing" or "delivery" as key
381
	 */
382
	public function getAddresses() : \Aimeos\Map
383
	{
384
		return map( $this->addresses );
385
	}
386
387
388
	/**
389
	 * Returns all address items as plain list
390
	 *
391
	 * @return \Aimeos\Map Associative list of address items implementing \Aimeos\MShop\Order\Item\Address\Iface
392
	 */
393
	public function getAddressItems() : \Aimeos\Map
394
	{
395
		return map( $this->addresses )->flat( 1 );
396
	}
397
398
399
	/**
400
	 * Replaces all addresses in the current basket with the new ones
401
	 *
402
	 * @param \Aimeos\Map|array $map Associative list of order addresses as returned by getAddresses()
403
	 * @return \Aimeos\MShop\Order\Item\Iface Order base item for method chaining
404
	 */
405
	public function setAddresses( iterable $map ) : \Aimeos\MShop\Order\Item\Iface
406
	{
407
		$map = $this->notify( 'setAddresses.before', $map );
408
409
		foreach( $map as $type => $items ) {
410
			$this->checkAddresses( $items, $type );
411
		}
412
413
		$old = $this->addresses;
414
		$this->addresses = is_map( $map ) ? $map->toArray() : $map;
415
		$this->setModified();
416
417
		$this->notify( 'setAddresses.after', $old );
418
419
		return $this;
420
	}
421
422
423
	/**
424
	 * Adds a coupon code and the given product item to the basket
425
	 *
426
	 * @param string $code Coupon code
427
	 * @return \Aimeos\MShop\Order\Item\Iface Order base item for method chaining
428
	 */
429
	public function addCoupon( string $code ) : \Aimeos\MShop\Order\Item\Iface
430
	{
431
		if( !isset( $this->coupons[$code] ) )
432
		{
433
			$code = $this->notify( 'addCoupon.before', $code );
434
435
			$this->coupons[$code] = [];
436
			$this->setModified();
437
438
			$this->notify( 'addCoupon.after', $code );
439
		}
440
441
		return $this;
442
	}
443
444
445
	/**
446
	 * Removes a coupon and the related product items from the basket
447
	 *
448
	 * @param string $code Coupon code
449
	 * @return \Aimeos\MShop\Order\Item\Iface Order base item for method chaining
450
	 */
451
	public function deleteCoupon( string $code ) : \Aimeos\MShop\Order\Item\Iface
452
	{
453
		if( isset( $this->coupons[$code] ) )
454
		{
455
			$old = [$code => $this->coupons[$code]];
456
			$old = $this->notify( 'deleteCoupon.before', $old );
457
458
			foreach( $this->coupons[$code] as $product )
459
			{
460
				if( ( $key = array_search( $product, $this->products, true ) ) !== false ) {
461
					unset( $this->products[$key] );
462
				}
463
			}
464
465
			unset( $this->coupons[$code] );
466
			$this->setModified();
467
468
			$this->notify( 'deleteCoupon.after', $old );
469
		}
470
471
		return $this;
472
	}
473
474
475
	/**
476
	 * Returns the available coupon codes and the lists of affected product items
477
	 *
478
	 * @return \Aimeos\Map Associative array of codes and lists of product items
479
	 *  implementing \Aimeos\MShop\Order\Product\Iface
480
	 */
481
	public function getCoupons() : \Aimeos\Map
482
	{
483
		return map( $this->coupons );
484
	}
485
486
487
	/**
488
	 * Sets a coupon code and the given product items in the basket.
489
	 *
490
	 * @param string $code Coupon code
491
	 * @param \Aimeos\MShop\Order\Item\Product\Iface[] $products List of coupon products
492
	 * @return \Aimeos\MShop\Order\Item\Iface Order base item for method chaining
493
	 */
494
	public function setCoupon( string $code, iterable $products = [] ) : \Aimeos\MShop\Order\Item\Iface
495
	{
496
		$new = $this->notify( 'setCoupon.before', [$code => $products] );
497
498
		$products = $this->checkProducts( map( $new )->first( [] ) );
499
500
		if( isset( $this->coupons[$code] ) )
501
		{
502
			foreach( $this->coupons[$code] as $product )
503
			{
504
				if( ( $key = array_search( $product, $this->products, true ) ) !== false ) {
505
					unset( $this->products[$key] );
506
				}
507
			}
508
		}
509
510
		foreach( $products as $product ) {
511
			$this->products[] = $product;
512
		}
513
514
		$old = isset( $this->coupons[$code] ) ? [$code => $this->coupons[$code]] : [];
515
		$this->coupons[$code] = is_map( $products ) ? $products->toArray() : $products;
516
		$this->setModified();
517
518
		$this->notify( 'setCoupon.after', $old );
519
520
		return $this;
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 );
533
534
		foreach( $map as $code => $products ) {
535
			$map[$code] = $this->checkProducts( $products );
536
		}
537
538
		foreach( $this->coupons as $code => $products )
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 )
549
		{
550
			foreach( $products as $product ) {
551
				$this->products[] = $product;
552
			}
553
		}
554
555
		$old = $this->coupons;
556
		$this->coupons = is_map( $map ) ? $map->toArray() : $map;
557
		$this->setModified();
558
559
		$this->notify( 'setCoupons.after', $old );
560
561
		return $this;
562
	}
563
564
565
	/**
566
	 * Adds an order product item to the basket
567
	 * If a similar item is found, only the quantity is increased.
568
	 *
569
	 * @param \Aimeos\MShop\Order\Item\Product\Iface $item Order product item to be added
570
	 * @param int|null $position position of the new order product item
571
	 * @return \Aimeos\MShop\Order\Item\Iface Order base item for method chaining
572
	 */
573
	public function addProduct( \Aimeos\MShop\Order\Item\Product\Iface $item, int $position = null ) : \Aimeos\MShop\Order\Item\Iface
574
	{
575
		$item = $this->notify( 'addProduct.before', $item );
576
577
		$this->checkProducts( [$item] );
578
579
		if( $position !== null ) {
580
			$this->products[$position] = $item;
581
		} elseif( ( $pos = $this->getSameProduct( $item, $this->products ) ) !== null ) {
582
			$item = $this->products[$pos]->setQuantity( $this->products[$pos]->getQuantity() + $item->getQuantity() );
583
		} else {
584
			$this->products[] = $item;
585
		}
586
587
		ksort( $this->products );
588
		$this->setModified();
589
590
		$this->notify( 'addProduct.after', $item );
591
592
		return $this;
593
	}
594
595
596
	/**
597
	 * Deletes an order product item from the basket
598
	 *
599
	 * @param int $position Position of the order product item
600
	 * @return \Aimeos\MShop\Order\Item\Iface Order base item for method chaining
601
	 */
602
	public function deleteProduct( int $position ) : \Aimeos\MShop\Order\Item\Iface
603
	{
604
		if( isset( $this->products[$position] ) )
605
		{
606
			$old = $this->products[$position];
607
			$old = $this->notify( 'deleteProduct.before', $old );
608
609
			unset( $this->products[$position] );
610
			$this->setModified();
611
612
			$this->notify( 'deleteProduct.after', $old );
613
		}
614
615
		return $this;
616
	}
617
618
619
	/**
620
	 * Returns the product item of an basket specified by its key
621
	 *
622
	 * @param int $key Key returned by getProducts() identifying the requested product
623
	 * @return \Aimeos\MShop\Order\Item\Product\Iface Product item of an order
624
	 */
625
	public function getProduct( int $key ) : \Aimeos\MShop\Order\Item\Product\Iface
626
	{
627
		if( !isset( $this->products[$key] ) ) {
628
			throw new \Aimeos\MShop\Order\Exception( sprintf( 'Product not available' ) );
629
		}
630
631
		return $this->products[$key];
632
	}
633
634
635
	/**
636
	 * Returns the product items that are or should be part of a basket
637
	 *
638
	 * @return \Aimeos\Map List of order product items implementing \Aimeos\MShop\Order\Item\Product\Iface
639
	 */
640
	public function getProducts() : \Aimeos\Map
641
	{
642
		return map( $this->products );
643
	}
644
645
646
	/**
647
	 * Replaces all products in the current basket with the new ones
648
	 *
649
	 * @param \Aimeos\MShop\Order\Item\Product\Iface[] $map Associative list of ordered products as returned by getProducts()
650
	 * @return \Aimeos\MShop\Order\Item\Iface Order base item for method chaining
651
	 */
652
	public function setProducts( iterable $map ) : \Aimeos\MShop\Order\Item\Iface
653
	{
654
		$map = $this->notify( 'setProducts.before', $map );
655
656
		$this->checkProducts( $map );
657
658
		$old = $this->products;
659
		$this->products = is_map( $map ) ? $map->toArray() : $map;
660
		$this->setModified();
661
662
		$this->notify( 'setProducts.after', $old );
663
664
		return $this;
665
	}
666
667
668
	/**
669
	 * Adds an order service to the basket
670
	 *
671
	 * @param \Aimeos\MShop\Order\Item\Service\Iface $service Order service item for the given domain
672
	 * @param string $type Service type constant from \Aimeos\MShop\Order\Item\Service\Base
673
	 * @param int|null $position Position of the service in the list to overwrite
674
	 * @return \Aimeos\MShop\Order\Item\Iface Order base item for method chaining
675
	 */
676
	public function addService( \Aimeos\MShop\Order\Item\Service\Iface $service, string $type, int $position = null ) : \Aimeos\MShop\Order\Item\Iface
677
	{
678
		$service = $this->notify( 'addService.before', $service );
679
680
		$this->checkPrice( $service->getPrice() );
681
682
		$service = clone $service;
683
		$service = $service->setType( $type );
684
685
		if( $position !== null ) {
686
			$this->services[$type][$position] = $service;
687
		} else {
688
			$this->services[$type][] = $service;
689
		}
690
691
		$this->setModified();
692
693
		$this->notify( 'addService.after', $service );
694
695
		return $this;
696
	}
697
698
699
	/**
700
	 * Deletes an order service from the basket
701
	 *
702
	 * @param string $type Service type constant from \Aimeos\MShop\Order\Item\Service\Base
703
	 * @param int|null $position Position of the service in the list to delete
704
	 * @return \Aimeos\MShop\Order\Item\Iface Order base item for method chaining
705
	 */
706
	public function deleteService( string $type, int $position = null ) : \Aimeos\MShop\Order\Item\Iface
707
	{
708
		if( $position === null && isset( $this->services[$type] ) || isset( $this->services[$type][$position] ) )
0 ignored issues
show
introduced by
Consider adding parentheses for clarity. Current Interpretation: ($position === null && IssetNode) || IssetNode, Probably Intended Meaning: $position === null && (IssetNode || IssetNode)
Loading history...
709
		{
710
			$old = ( isset( $this->services[$type][$position] ) ? $this->services[$type][$position] : $this->services[$type] );
711
			$old = $this->notify( 'deleteService.before', $old );
712
713
			if( $position !== null ) {
714
				unset( $this->services[$type][$position] );
715
			} else {
716
				unset( $this->services[$type] );
717
			}
718
719
			$this->setModified();
720
721
			$this->notify( 'deleteService.after', $old );
722
		}
723
724
		return $this;
725
	}
726
727
728
	/**
729
	 * Returns the order services depending on the given type
730
	 *
731
	 * @param string $type Service type constant from \Aimeos\MShop\Order\Item\Service\Base
732
	 * @param int|null $position Position of the service in the list to retrieve
733
	 * @return \Aimeos\MShop\Order\Item\Service\Iface[]|\Aimeos\MShop\Order\Item\Service\Iface
734
	 * 	Order service item or list of items for the requested type
735
	 * @throws \Aimeos\MShop\Order\Exception If no service for the given type and position is found
736
	 */
737
	public function getService( string $type, int $position = null )
738
	{
739
		if( $position !== null )
740
		{
741
			if( isset( $this->services[$type][$position] ) ) {
742
				return $this->services[$type][$position];
743
			}
744
745
			throw new \Aimeos\MShop\Order\Exception( sprintf( 'Service not available' ) );
746
		}
747
748
		return ( isset( $this->services[$type] ) ? $this->services[$type] : [] );
749
	}
750
751
752
	/**
753
	 * Returns all services that are part of the basket
754
	 *
755
	 * @return \Aimeos\Map Associative list of service types ("delivery" or "payment") as keys and list of
756
	 *	service items implementing \Aimeos\MShop\Order\Service\Iface as values
757
	 */
758
	public function getServices() : \Aimeos\Map
759
	{
760
		return map( $this->services );
761
	}
762
763
764
	/**
765
	 * Returns all service items as plain list
766
	 *
767
	 * @return \Aimeos\Map List of service items implementing \Aimeos\MShop\Order\Service\Iface
768
	 */
769
	public function getServiceItems() : \Aimeos\Map
770
	{
771
		return map( $this->services )->flat( 1 );
772
	}
773
774
775
	/**
776
	 * Replaces all services in the current basket with the new ones
777
	 *
778
	 * @param \Aimeos\MShop\Order\Item\Service\Iface[] $map Associative list of order services as returned by getServices()
779
	 * @return \Aimeos\MShop\Order\Item\Iface Order base item for method chaining
780
	 */
781
	public function setServices( iterable $map ) : \Aimeos\MShop\Order\Item\Iface
782
	{
783
		$map = $this->notify( 'setServices.before', $map );
784
785
		foreach( $map as $type => $services ) {
786
			$map[$type] = $this->checkServices( $services, $type );
787
		}
788
789
		$old = $this->services;
790
		$this->services = is_map( $map ) ? $map->toArray() : $map;
791
		$this->setModified();
792
793
		$this->notify( 'setServices.after', $old );
794
795
		return $this;
796
	}
797
798
799
	/**
800
	 * Adds a status item to the order
801
	 *
802
	 * @param \Aimeos\MShop\Order\Item\Status\Iface $item Order status item
803
	 * @return \Aimeos\MShop\Order\Item\Iface Order item for method chaining
804
	 */
805
	public function addStatus( \Aimeos\MShop\Order\Item\Status\Iface $item ) : \Aimeos\MShop\Order\Item\Iface
806
	{
807
		$type = $item->getType();
808
		$value = $item->getValue();
809
810
		if( isset( $this->statuses[$type][$value] ) ) {
811
			$this->statuses[$type][$value] = $item->setId( $this->statuses[$type][$value]->getId() );
812
		} else {
813
			$this->statuses[$type][$value] = $item;
814
		}
815
816
		return $this->setModified();
817
	}
818
819
820
	/**
821
	 * Returns the status item specified by its type and value
822
	 *
823
	 * @param string $type Status type
824
	 * @param string $value Status value
825
	 * @return \Aimeos\MShop\Order\Item\Status\Iface Status item of an order
826
	 * @throws \Aimeos\MShop\Order\Exception If status item is not available
827
	 */
828
	public function getStatus( string $type, string $value ) : \Aimeos\MShop\Order\Item\Status\Iface
829
	{
830
		if( !isset( $this->statuses[$type][$value] ) ) {
831
			throw new \Aimeos\MShop\Order\Exception( sprintf( 'Status not available' ) );
832
		}
833
834
		return $this->statuses[$type][$value];
835
	}
836
837
838
	/**
839
	 * Returns the status items
840
	 *
841
	 * @return \Aimeos\Map Associative list of status types as keys and list of
842
	 *	status value/item pairs implementing \Aimeos\MShop\Order\Status\Iface as values
843
	 */
844
	public function getStatuses() : \Aimeos\Map
845
	{
846
		return map( $this->statuses );
847
	}
848
849
850
	/**
851
	 * Returns all status items as plain list
852
	 *
853
	 * @return \Aimeos\Map List of status items implementing \Aimeos\MShop\Order\Status\Iface
854
	 */
855
	public function getStatusItems() : \Aimeos\Map
856
	{
857
		return map( $this->statuses )->flat( 1 );
858
	}
859
860
861
	/**
862
	 * Returns the service costs
863
	 *
864
	 * @param string $type Service type like "delivery" or "payment"
865
	 * @return float Service costs value
866
	 */
867
	public function getCosts( string $type = 'delivery' ) : float
868
	{
869
		$costs = 0;
870
871
		if( $type === 'delivery' )
872
		{
873
			foreach( $this->getProducts() as $product ) {
874
				$costs += $product->getPrice()->getCosts() * $product->getQuantity();
875
			}
876
		}
877
878
		foreach( $this->getService( $type ) as $service ) {
879
			$costs += $service->getPrice()->getCosts();
880
		}
881
882
		return $costs;
883
	}
884
885
886
	/**
887
	 * Returns a price item with amounts calculated for the products, costs, etc.
888
	 *
889
	 * @return \Aimeos\MShop\Price\Item\Iface Price item with price, costs and rebate the customer has to pay
890
	 */
891
	public function getPrice() : \Aimeos\MShop\Price\Item\Iface
892
	{
893
		if( $this->recalc )
894
		{
895
			$price = $this->price->clear();
896
897
			foreach( $this->getServices() as $list )
898
			{
899
				foreach( $list as $service ) {
900
					$price = $price->addItem( $service->getPrice() );
901
				}
902
			}
903
904
			foreach( $this->getProducts() as $product ) {
905
				$price = $price->addItem( $product->getPrice(), $product->getQuantity() );
906
			}
907
908
			$this->price = $price;
909
			$this->recalc = false;
910
		}
911
912
		return $this->price;
913
	}
914
915
916
	/**
917
	 * Returns a list of tax names and values
918
	 *
919
	 * @return array Associative list of tax names as key and price items as value
920
	 */
921
	public function getTaxes() : array
922
	{
923
		$taxes = [];
924
925
		foreach( $this->getProducts() as $product )
926
		{
927
			$price = $product->getPrice();
928
929
			foreach( $price->getTaxrates() as $name => $taxrate )
930
			{
931
				$price = (clone $price)->setTaxRate( $taxrate );
932
933
				if( isset( $taxes[$name][$taxrate] ) ) {
934
					$taxes[$name][$taxrate]->addItem( $price, $product->getQuantity() );
935
				} else {
936
					$taxes[$name][$taxrate] = $price->addItem( $price, $product->getQuantity() - 1 );
937
				}
938
			}
939
		}
940
941
		foreach( $this->getServices() as $services )
942
		{
943
			foreach( $services as $service )
944
			{
945
				$price = $service->getPrice();
946
947
				foreach( $price->getTaxrates() as $name => $taxrate )
948
				{
949
					$price = (clone $price)->setTaxRate( $taxrate );
950
951
					if( isset( $taxes[$name][$taxrate] ) ) {
952
						$taxes[$name][$taxrate]->addItem( $price );
953
					} else {
954
						$taxes[$name][$taxrate] = $price;
955
					}
956
				}
957
			}
958
		}
959
960
		return $taxes;
961
	}
962
963
964
	/**
965
	 * Returns the locales for the basic order item.
966
	 *
967
	 * @return \Aimeos\MShop\Locale\Item\Iface Object containing information
968
	 *  about site, language, country and currency
969
	 */
970
	public function locale() : \Aimeos\MShop\Locale\Item\Iface
971
	{
972
		return $this->get( '.locale' );
973
	}
974
975
976
	/**
977
	 * Sets the locales for the basic order item.
978
	 *
979
	 * @param \Aimeos\MShop\Locale\Item\Iface $locale Object containing information
980
	 *  about site, language, country and currency
981
	 * @return \Aimeos\MShop\Order\Item\Iface Order base item for chaining method calls
982
	 */
983
	public function setLocale( \Aimeos\MShop\Locale\Item\Iface $locale ) : \Aimeos\MShop\Order\Item\Iface
984
	{
985
		$this->notify( 'setLocale.before', $locale );
986
		$this->set( '.locale', clone $locale );
987
		$this->notify( 'setLocale.after', $locale );
988
989
		return parent::setModified();
990
	}
991
992
993
	/**
994
	 * Sets the modified flag of the object.
995
	 *
996
	 * @return \Aimeos\MShop\Common\Item\Iface Order base item for method chaining
997
	 */
998
	public function setModified() : \Aimeos\MShop\Common\Item\Iface
999
	{
1000
		$this->recalc = true;
1001
		return parent::setModified();
1002
	}
1003
1004
1005
	/**
1006
	 * Returns the item values as array.
1007
	 *
1008
	 * @param bool True to return private properties, false for public only
1009
	 * @return array Associative list of item properties and their values
1010
	 */
1011
	public function toArray( bool $private = false ) : array
1012
	{
1013
		$price = $this->getPrice();
1014
		$list = parent::toArray( $private );
1015
1016
		$list['order.currencyid'] = $price->getCurrencyId();
1017
		$list['order.price'] = $price->getValue();
1018
		$list['order.costs'] = $price->getCosts();
1019
		$list['order.rebate'] = $price->getRebate();
1020
		$list['order.taxflag'] = $price->getTaxFlag();
1021
		$list['order.taxvalue'] = $price->getTaxValue();
1022
1023
		return $list;
1024
	}
1025
1026
1027
	/**
1028
	 * Checks if the given delivery status is a valid constant.
1029
	 *
1030
	 * @param int $value Delivery status constant defined in \Aimeos\MShop\Order\Item\Base
1031
	 * @return int Delivery status constant defined in \Aimeos\MShop\Order\Item\Base
1032
	 * @throws \Aimeos\MShop\Order\Exception If delivery status is invalid
1033
	 */
1034
	protected function checkDeliveryStatus( int $value )
1035
	{
1036
		if( $value < \Aimeos\MShop\Order\Item\Base::STAT_UNFINISHED || $value > \Aimeos\MShop\Order\Item\Base::STAT_RETURNED ) {
1037
			throw new \Aimeos\MShop\Order\Exception( sprintf( 'Order delivery status "%1$s" not within allowed range', $value ) );
1038
		}
1039
1040
		return $value;
1041
	}
1042
1043
1044
	/**
1045
	 * Checks the given payment status is a valid constant.
1046
	 *
1047
	 * @param int $value Payment status constant defined in \Aimeos\MShop\Order\Item\Base
1048
	 * @return int Payment status constant defined in \Aimeos\MShop\Order\Item\Base
1049
	 * @throws \Aimeos\MShop\Order\Exception If payment status is invalid
1050
	 */
1051
	protected function checkPaymentStatus( int $value )
1052
	{
1053
		if( $value < \Aimeos\MShop\Order\Item\Base::PAY_UNFINISHED || $value > \Aimeos\MShop\Order\Item\Base::PAY_RECEIVED ) {
1054
			throw new \Aimeos\MShop\Order\Exception( sprintf( 'Order payment status "%1$s" not within allowed range', $value ) );
1055
		}
1056
1057
		return $value;
1058
	}
1059
1060
1061
	/**
1062
	 * Checks if the price uses the same currency as the price in the basket.
1063
	 *
1064
	 * @param \Aimeos\MShop\Price\Item\Iface $item Price item
1065
	 */
1066
	protected function checkPrice( \Aimeos\MShop\Price\Item\Iface $item )
1067
	{
1068
		$price = clone $this->getPrice();
1069
		$price->addItem( $item );
1070
	}
1071
1072
1073
	/**
1074
	 * Checks if all order addresses are valid
1075
	 *
1076
	 * @param \Aimeos\MShop\Order\Item\Address\Iface[] $items Order address items
1077
	 * @param string $type Address type constant from \Aimeos\MShop\Order\Item\Address\Base
1078
	 * @return \Aimeos\MShop\Order\Item\Address\Iface[] List of checked items
1079
	 * @throws \Aimeos\MShop\Exception If one of the order addresses is invalid
1080
	 */
1081
	protected function checkAddresses( iterable $items, string $type ) : iterable
1082
	{
1083
		map( $items )->implements( \Aimeos\MShop\Order\Item\Address\Iface::class, true );
1084
1085
		foreach( $items as $key => $item ) {
1086
			$items[$key] = $item->setType( $type );
1087
		}
1088
1089
		return $items;
1090
	}
1091
1092
1093
	/**
1094
	 * Checks if all order products are valid
1095
	 *
1096
	 * @param \Aimeos\MShop\Order\Item\Product\Iface[] $items Order product items
1097
	 * @return \Aimeos\MShop\Order\Item\Product\Iface[] List of checked items
1098
	 * @throws \Aimeos\MShop\Exception If one of the order products is invalid
1099
	 */
1100
	protected function checkProducts( iterable $items ) : \Aimeos\Map
1101
	{
1102
		map( $items )->implements( \Aimeos\MShop\Order\Item\Product\Iface::class, true );
1103
1104
		foreach( $items as $key => $item )
1105
		{
1106
			if( $item->getProductCode() === '' ) {
1107
				throw new \Aimeos\MShop\Order\Exception( sprintf( 'Product does not contain the SKU code' ) );
1108
			}
1109
1110
			$this->checkPrice( $item->getPrice() );
1111
		}
1112
1113
		return map( $items );
1114
	}
1115
1116
1117
	/**
1118
	 * Checks if all order services are valid
1119
	 *
1120
	 * @param \Aimeos\MShop\Order\Item\Service\Iface[] $items Order service items
1121
	 * @param string $type Service type constant from \Aimeos\MShop\Order\Item\Service\Base
1122
	 * @return \Aimeos\MShop\Order\Item\Service\Iface[] List of checked items
1123
	 * @throws \Aimeos\MShop\Exception If one of the order services is invalid
1124
	 */
1125
	protected function checkServices( iterable $items, string $type ) : iterable
1126
	{
1127
		map( $items )->implements( \Aimeos\MShop\Order\Item\Service\Iface::class, true );
1128
1129
		foreach( $items as $key => $item )
1130
		{
1131
			$this->checkPrice( $item->getPrice() );
1132
			$items[$key] = $item->setType( $type );
1133
		}
1134
1135
		return $items;
1136
	}
1137
1138
1139
	/**
1140
	 * Tests if the given product is similar to an existing one.
1141
	 * Similarity is described by the equality of properties so the quantity of
1142
	 * the existing product can be updated.
1143
	 *
1144
	 * @param \Aimeos\MShop\Order\Item\Product\Iface $item Order product item
1145
	 * @param \Aimeos\MShop\Order\Item\Product\Iface[] $products List of order product items to check against
1146
	 * @return int|null Positon of the same product in the product list of false if product is unique
1147
	 * @throws \Aimeos\MShop\Order\Exception If no similar item was found
1148
	 */
1149
	protected function getSameProduct( \Aimeos\MShop\Order\Item\Product\Iface $item, iterable $products ) : ?int
1150
	{
1151
		$map = [];
1152
		$count = 0;
1153
1154
		foreach( $item->getAttributeItems() as $attributeItem )
1155
		{
1156
			$key = md5( $attributeItem->getCode() . json_encode( $attributeItem->getValue() ) );
1157
			$map[$key] = $attributeItem;
1158
			$count++;
1159
		}
1160
1161
		foreach( $products as $position => $product )
1162
		{
1163
			if( $product->compare( $item ) === false ) {
1164
				continue;
1165
			}
1166
1167
			$prodAttributes = $product->getAttributeItems();
1168
1169
			if( count( $prodAttributes ) !== $count ) {
1170
				continue;
1171
			}
1172
1173
			foreach( $prodAttributes as $attribute )
1174
			{
1175
				$key = md5( $attribute->getCode() . json_encode( $attribute->getValue() ) );
1176
1177
				if( isset( $map[$key] ) === false || $map[$key]->getQuantity() != $attribute->getQuantity() ) {
1178
					continue 2; // jump to outer loop
1179
				}
1180
			}
1181
1182
			return $position;
1183
		}
1184
1185
		return null;
1186
	}
1187
}
1188