Passed
Push — master ( b35ffc...19a63c )
by Aimeos
04:50
created

Base::__sleep()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 2
c 0
b 0
f 0
dl 0
loc 9
rs 10
cc 1
nc 1
nop 0
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-2022
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\MW\Observer\Publisher\Iface,
24
		\Aimeos\Macro\Iface, \ArrayAccess, \JsonSerializable
25
{
26
	use \Aimeos\MW\Observer\Publisher\Traits;
27
	use \Aimeos\Macro\Macroable;
28
29
30
	/**
31
	 * Unfinished delivery.
32
	 * This is the default status after creating an order and this status
33
	 * should be also used as long as technical errors occurs.
34
	 */
35
	const STAT_UNFINISHED = -1;
36
37
	/**
38
	 * Delivery was deleted.
39
	 * The delivery of the order was deleted manually.
40
	 */
41
	const STAT_DELETED = 0;
42
43
	/**
44
	 * Delivery is pending.
45
	 * The order is not yet in the fulfillment process until further actions
46
	 * are taken.
47
	 */
48
	const STAT_PENDING = 1;
49
50
	/**
51
	 * Fulfillment in progress.
52
	 * The delivery of the order is in the (internal) fulfillment process and
53
	 * will be ready soon.
54
	 */
55
	const STAT_PROGRESS = 2;
56
57
	/**
58
	 * Parcel is dispatched.
59
	 * The parcel was given to the logistic partner for delivery to the
60
	 * customer.
61
	 */
62
	const STAT_DISPATCHED = 3;
63
64
	/**
65
	 * Parcel was delivered.
66
	 * The logistic partner delivered the parcel and the customer received it.
67
	 */
68
	const STAT_DELIVERED = 4;
69
70
	/**
71
	 * Parcel is lost.
72
	 * The parcel is lost during delivery by the logistic partner and haven't
73
	 * reached the customer nor it's returned to the merchant.
74
	 */
75
	const STAT_LOST = 5;
76
77
	/**
78
	 * Parcel was refused.
79
	 * The delivery of the parcel failed because the customer has refused to
80
	 * accept it or the address was invalid.
81
	 */
82
	const STAT_REFUSED = 6;
83
84
	/**
85
	 * Parcel was returned.
86
	 * The parcel was sent back by the customer.
87
	 */
88
	const STAT_RETURNED = 7;
89
90
91
	/**
92
	 * Unfinished payment.
93
	 * This is the default status after creating an order and this status
94
	 * should be also used as long as technical errors occurs.
95
	 */
96
	const PAY_UNFINISHED = -1;
97
98
	/**
99
	 * Payment was deleted.
100
	 * The payment for the order was deleted manually.
101
	 */
102
	const PAY_DELETED = 0;
103
104
	/**
105
	 * Payment was canceled.
106
	 * The customer canceled the payment process.
107
	 */
108
	const PAY_CANCELED = 1;
109
110
	/**
111
	 * Payment was refused.
112
	 * The customer didn't enter valid payment details.
113
	 */
114
	const PAY_REFUSED = 2;
115
116
	/**
117
	 * Payment was refund.
118
	 * The payment was OK but refund and the customer got his money back.
119
	 */
120
	const PAY_REFUND = 3;
121
122
	/**
123
	 * Payment is pending.
124
	 * The payment is not yet done until further actions are taken.
125
	 */
126
	const PAY_PENDING = 4;
127
128
	/**
129
	 * Payment is authorized.
130
	 * The customer authorized the merchant to invoice the amount but the money
131
	 * is not yet received. This is used for all post-paid orders.
132
	 */
133
	const PAY_AUTHORIZED = 5;
134
135
	/**
136
	 * Payment is received.
137
	 * The merchant received the money from the customer.
138
	 */
139
	const PAY_RECEIVED = 6;
140
141
	/**
142
	 * Payment is transferred.
143
	 * The vendor received the money from the platform.
144
	 */
145
	const PAY_TRANSFERRED = 7;
146
147
148
	// protected is a workaround for serialize problem
149
	protected $coupons;
150
	protected $products;
151
	protected $services = [];
152
	protected $addresses = [];
153
154
155
	/**
156
	 * Initializes the basket object
157
	 *
158
	 * @param \Aimeos\MShop\Price\Item\Iface $price Default price of the basket (usually 0.00)
159
	 * @param \Aimeos\MShop\Locale\Item\Iface $locale Locale item containing the site, language and currency
160
	 * @param array $values Associative list of key/value pairs containing, e.g. the order or user ID
161
	 * @param array $products List of ordered products implementing \Aimeos\MShop\Order\Item\Product\Iface
162
	 * @param array $addresses List of order addresses implementing \Aimeos\MShop\Order\Item\Address\Iface
163
	 * @param array $services List of order services implementing \Aimeos\MShop\Order\Item\Service\Iface
164
	 * @param array $coupons Associative list of coupon codes as keys and ordered products implementing \Aimeos\MShop\Order\Item\Product\Iface as values
165
	 */
166
	public function __construct( \Aimeos\MShop\Price\Item\Iface $price, \Aimeos\MShop\Locale\Item\Iface $locale,
167
		array $values = [], array $products = [], array $addresses = [],
168
		array $services = [], array $coupons = [] )
169
	{
170
		map( $addresses )->implements( \Aimeos\MShop\Order\Item\Address\Iface::class, true );
171
		map( $products )->implements( \Aimeos\MShop\Order\Item\Product\Iface::class, true );
172
		map( $services )->implements( \Aimeos\MShop\Order\Item\Service\Iface::class, true );
173
174
		foreach( $coupons as $couponProducts ) {
175
			map( $couponProducts )->implements( \Aimeos\MShop\Order\Item\Product\Iface::class, true );
176
		}
177
178
		parent::__construct( 'order.', $values );
179
180
		$this->coupons = $coupons;
181
		$this->products = $products;
182
183
		foreach( $addresses as $address ) {
184
			$this->addresses[$address->getType()][] = $address;
185
		}
186
187
		foreach( $services as $service ) {
188
			$this->services[$service->getType()][] = $service;
189
		}
190
	}
191
192
193
	/**
194
	 * Specifies the data which should be serialized to JSON by json_encode().
195
	 *
196
	 * @return array<string,mixed> Data to serialize to JSON
197
	 */
198
	#[\ReturnTypeWillChange]
199
	public function jsonSerialize()
200
	{
201
		return parent::jsonSerialize() + [
202
			'coupons' => $this->coupons,
203
			'products' => $this->products,
204
			'services' => $this->services,
205
			'addresses' => $this->addresses,
206
		];
207
	}
208
209
210
	/**
211
	 * Prepares the object for serialization.
212
	 *
213
	 * @return array List of properties that should be serialized
214
	 */
215
	public function __sleep() : array
216
	{
217
		/*
218
		 * Workaround because database connections can't be serialized
219
		 * Listeners will be reattached on wakeup by the order base manager
220
		 */
221
		$this->off();
222
223
		return array_keys( get_object_vars( $this ) );
224
	}
225
226
227
	/**
228
	 * Returns the ID of the items
229
	 *
230
	 * @return string ID of the item or null
231
	 */
232
	public function __toString() : string
233
	{
234
		return (string) $this->getId();
235
	}
236
237
238
	/**
239
	 * Tests if all necessary items are available to create the order.
240
	 *
241
	 * @param array $what Type of data
242
	 * @return \Aimeos\MShop\Order\Item\Iface Order base item for method chaining
243
	 * @throws \Aimeos\MShop\Order\Exception if there are no products in the basket
244
	 */
245
	public function check( array $what = ['order/address', 'order/coupon', 'order/product', 'order/service'] ) : \Aimeos\MShop\Order\Item\Iface
246
	{
247
		$this->notify( 'check.before', $what );
248
249
		if( in_array( 'order/product', $what ) && ( count( $this->getProducts() ) < 1 ) ) {
250
			throw new \Aimeos\MShop\Order\Exception( sprintf( 'Basket empty' ) );
251
		}
252
253
		$this->notify( 'check.after', $what );
254
255
		return $this;
256
	}
257
258
259
	/**
260
	 * Notifies listeners before the basket becomes an order.
261
	 *
262
	 * @return \Aimeos\MShop\Order\Item\Iface Order base item for chaining method calls
263
	 */
264
	public function finish() : \Aimeos\MShop\Order\Item\Iface
265
	{
266
		$this->notify( 'setOrder.before' );
267
		return $this;
268
	}
269
270
271
	/**
272
	 * Returns the item type
273
	 *
274
	 * @return string Item type, subtypes are separated by slashes
275
	 */
276
	public function getResourceType() : string
277
	{
278
		return 'order';
279
	}
280
281
282
	/**
283
	 * Adds the address of the given type to the basket
284
	 *
285
	 * @param \Aimeos\MShop\Order\Item\Address\Iface $address Order address item for the given type
286
	 * @param string $type Address type, usually "billing" or "delivery"
287
	 * @param int|null $position Position of the address in the list
288
	 * @return \Aimeos\MShop\Order\Item\Iface Order base item for method chaining
289
	 */
290
	public function addAddress( \Aimeos\MShop\Order\Item\Address\Iface $address, string $type, int $position = null ) : \Aimeos\MShop\Order\Item\Iface
291
	{
292
		$address = $this->notify( 'addAddress.before', $address );
293
294
		$address = clone $address;
295
		$address = $address->setType( $type );
296
297
		if( $position !== null ) {
298
			$this->addresses[$type][$position] = $address;
299
		} else {
300
			$this->addresses[$type][] = $address;
301
		}
302
303
		$this->setModified();
304
305
		$this->notify( 'addAddress.after', $address );
306
307
		return $this;
308
	}
309
310
311
	/**
312
	 * Deletes an order address from the basket
313
	 *
314
	 * @param string $type Address type defined in \Aimeos\MShop\Order\Item\Address\Base
315
	 * @param int|null $position Position of the address in the list
316
	 * @return \Aimeos\MShop\Order\Item\Iface Order base item for method chaining
317
	 */
318
	public function deleteAddress( string $type, int $position = null ) : \Aimeos\MShop\Order\Item\Iface
319
	{
320
		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...
321
		{
322
			$old = ( isset( $this->addresses[$type][$position] ) ? $this->addresses[$type][$position] : $this->addresses[$type] );
323
			$old = $this->notify( 'deleteAddress.before', $old );
324
325
			if( $position !== null ) {
326
				unset( $this->addresses[$type][$position] );
327
			} else {
328
				unset( $this->addresses[$type] );
329
			}
330
331
			$this->setModified();
332
333
			$this->notify( 'deleteAddress.after', $old );
334
		}
335
336
		return $this;
337
	}
338
339
340
	/**
341
	 * Returns the order address depending on the given type
342
	 *
343
	 * @param string $type Address type, usually "billing" or "delivery"
344
	 * @param int|null $position Address position in list of addresses
345
	 * @return \Aimeos\MShop\Order\Item\Address\Iface[]|\Aimeos\MShop\Order\Item\Address\Iface Order address item or list of
346
	 */
347
	public function getAddress( string $type, int $position = null )
348
	{
349
		if( $position !== null )
350
		{
351
			if( isset( $this->addresses[$type][$position] ) ) {
352
				return $this->addresses[$type][$position];
353
			}
354
355
			throw new \Aimeos\MShop\Order\Exception( sprintf( 'Address not available' ) );
356
		}
357
358
		return ( isset( $this->addresses[$type] ) ? $this->addresses[$type] : [] );
359
	}
360
361
362
	/**
363
	 * Returns all addresses that are part of the basket
364
	 *
365
	 * @return \Aimeos\Map Associative list of address items implementing
366
	 *  \Aimeos\MShop\Order\Item\Address\Iface with "billing" or "delivery" as key
367
	 */
368
	public function getAddresses() : \Aimeos\Map
369
	{
370
		return map( $this->addresses );
371
	}
372
373
374
	/**
375
	 * Replaces all addresses in the current basket with the new ones
376
	 *
377
	 * @param \Aimeos\Map|array $map Associative list of order addresses as returned by getAddresses()
378
	 * @return \Aimeos\MShop\Order\Item\Iface Order base item for method chaining
379
	 */
380
	public function setAddresses( iterable $map ) : \Aimeos\MShop\Order\Item\Iface
381
	{
382
		$map = $this->notify( 'setAddresses.before', $map );
383
384
		foreach( $map as $type => $items ) {
385
			$this->checkAddresses( $items, $type );
386
		}
387
388
		$old = $this->addresses;
389
		$this->addresses = is_map( $map ) ? $map->toArray() : $map;
390
		$this->setModified();
391
392
		$this->notify( 'setAddresses.after', $old );
393
394
		return $this;
395
	}
396
397
398
	/**
399
	 * Adds a coupon code and the given product item to the basket
400
	 *
401
	 * @param string $code Coupon code
402
	 * @return \Aimeos\MShop\Order\Item\Iface Order base item for method chaining
403
	 */
404
	public function addCoupon( string $code ) : \Aimeos\MShop\Order\Item\Iface
405
	{
406
		if( !isset( $this->coupons[$code] ) )
407
		{
408
			$code = $this->notify( 'addCoupon.before', $code );
409
410
			$this->coupons[$code] = [];
411
			$this->setModified();
412
413
			$this->notify( 'addCoupon.after', $code );
414
		}
415
416
		return $this;
417
	}
418
419
420
	/**
421
	 * Removes a coupon and the related product items from the basket
422
	 *
423
	 * @param string $code Coupon code
424
	 * @return \Aimeos\MShop\Order\Item\Iface Order base item for method chaining
425
	 */
426
	public function deleteCoupon( string $code ) : \Aimeos\MShop\Order\Item\Iface
427
	{
428
		if( isset( $this->coupons[$code] ) )
429
		{
430
			$old = [$code => $this->coupons[$code]];
431
			$old = $this->notify( 'deleteCoupon.before', $old );
432
433
			foreach( $this->coupons[$code] as $product )
434
			{
435
				if( ( $key = array_search( $product, $this->products, true ) ) !== false ) {
436
					unset( $this->products[$key] );
437
				}
438
			}
439
440
			unset( $this->coupons[$code] );
441
			$this->setModified();
442
443
			$this->notify( 'deleteCoupon.after', $old );
444
		}
445
446
		return $this;
447
	}
448
449
450
	/**
451
	 * Returns the available coupon codes and the lists of affected product items
452
	 *
453
	 * @return \Aimeos\Map Associative array of codes and lists of product items
454
	 *  implementing \Aimeos\MShop\Order\Product\Iface
455
	 */
456
	public function getCoupons() : \Aimeos\Map
457
	{
458
		return map( $this->coupons );
459
	}
460
461
462
	/**
463
	 * Sets a coupon code and the given product items in the basket.
464
	 *
465
	 * @param string $code Coupon code
466
	 * @param \Aimeos\MShop\Order\Item\Product\Iface[] $products List of coupon products
467
	 * @return \Aimeos\MShop\Order\Item\Iface Order base item for method chaining
468
	 */
469
	public function setCoupon( string $code, iterable $products = [] ) : \Aimeos\MShop\Order\Item\Iface
470
	{
471
		$new = $this->notify( 'setCoupon.before', [$code => $products] );
472
473
		$products = $this->checkProducts( map( $new )->first( [] ) );
474
475
		if( isset( $this->coupons[$code] ) )
476
		{
477
			foreach( $this->coupons[$code] as $product )
478
			{
479
				if( ( $key = array_search( $product, $this->products, true ) ) !== false ) {
480
					unset( $this->products[$key] );
481
				}
482
			}
483
		}
484
485
		foreach( $products as $product ) {
486
			$this->products[] = $product;
487
		}
488
489
		$old = isset( $this->coupons[$code] ) ? [$code => $this->coupons[$code]] : [];
490
		$this->coupons[$code] = is_map( $products ) ? $products->toArray() : $products;
491
		$this->setModified();
492
493
		$this->notify( 'setCoupon.after', $old );
494
495
		return $this;
496
	}
497
498
499
	/**
500
	 * Replaces all coupons in the current basket with the new ones
501
	 *
502
	 * @param iterable $map Associative list of order coupons as returned by getCoupons()
503
	 * @return \Aimeos\MShop\Order\Item\Iface Order base item for method chaining
504
	 */
505
	public function setCoupons( iterable $map ) : \Aimeos\MShop\Order\Item\Iface
506
	{
507
		$map = $this->notify( 'setCoupons.before', $map );
508
509
		foreach( $map as $code => $products ) {
510
			$map[$code] = $this->checkProducts( $products );
511
		}
512
513
		foreach( $this->coupons as $code => $products )
514
		{
515
			foreach( $products as $product )
516
			{
517
				if( ( $key = array_search( $product, $this->products, true ) ) !== false ) {
518
					unset( $this->products[$key] );
519
				}
520
			}
521
		}
522
523
		foreach( $map as $code => $products )
524
		{
525
			foreach( $products as $product ) {
526
				$this->products[] = $product;
527
			}
528
		}
529
530
		$old = $this->coupons;
531
		$this->coupons = is_map( $map ) ? $map->toArray() : $map;
532
		$this->setModified();
533
534
		$this->notify( 'setCoupons.after', $old );
535
536
		return $this;
537
	}
538
539
540
	/**
541
	 * Adds an order product item to the basket
542
	 * If a similar item is found, only the quantity is increased.
543
	 *
544
	 * @param \Aimeos\MShop\Order\Item\Product\Iface $item Order product item to be added
545
	 * @param int|null $position position of the new order product item
546
	 * @return \Aimeos\MShop\Order\Item\Iface Order base item for method chaining
547
	 */
548
	public function addProduct( \Aimeos\MShop\Order\Item\Product\Iface $item, int $position = null ) : \Aimeos\MShop\Order\Item\Iface
549
	{
550
		$item = $this->notify( 'addProduct.before', $item );
551
552
		$this->checkProducts( [$item] );
553
554
		if( $position !== null ) {
555
			$this->products[$position] = $item;
556
		} elseif( ( $pos = $this->getSameProduct( $item, $this->products ) ) !== null ) {
557
			$item = $this->products[$pos]->setQuantity( $this->products[$pos]->getQuantity() + $item->getQuantity() );
558
		} else {
559
			$this->products[] = $item;
560
		}
561
562
		ksort( $this->products );
563
		$this->setModified();
564
565
		$this->notify( 'addProduct.after', $item );
566
567
		return $this;
568
	}
569
570
571
	/**
572
	 * Deletes an order product item from the basket
573
	 *
574
	 * @param int $position Position of the order product item
575
	 * @return \Aimeos\MShop\Order\Item\Iface Order base item for method chaining
576
	 */
577
	public function deleteProduct( int $position ) : \Aimeos\MShop\Order\Item\Iface
578
	{
579
		if( isset( $this->products[$position] ) )
580
		{
581
			$old = $this->products[$position];
582
			$old = $this->notify( 'deleteProduct.before', $old );
583
584
			unset( $this->products[$position] );
585
			$this->setModified();
586
587
			$this->notify( 'deleteProduct.after', $old );
588
		}
589
590
		return $this;
591
	}
592
593
594
	/**
595
	 * Returns the product item of an basket specified by its key
596
	 *
597
	 * @param int $key Key returned by getProducts() identifying the requested product
598
	 * @return \Aimeos\MShop\Order\Item\Product\Iface Product item of an order
599
	 */
600
	public function getProduct( int $key ) : \Aimeos\MShop\Order\Item\Product\Iface
601
	{
602
		if( !isset( $this->products[$key] ) ) {
603
			throw new \Aimeos\MShop\Order\Exception( sprintf( 'Product not available' ) );
604
		}
605
606
		return $this->products[$key];
607
	}
608
609
610
	/**
611
	 * Returns the product items that are or should be part of a basket
612
	 *
613
	 * @return \Aimeos\Map List of order product items implementing \Aimeos\MShop\Order\Item\Product\Iface
614
	 */
615
	public function getProducts() : \Aimeos\Map
616
	{
617
		return map( $this->products );
618
	}
619
620
621
	/**
622
	 * Replaces all products in the current basket with the new ones
623
	 *
624
	 * @param \Aimeos\MShop\Order\Item\Product\Iface[] $map Associative list of ordered products as returned by getProducts()
625
	 * @return \Aimeos\MShop\Order\Item\Iface Order base item for method chaining
626
	 */
627
	public function setProducts( iterable $map ) : \Aimeos\MShop\Order\Item\Iface
628
	{
629
		$map = $this->notify( 'setProducts.before', $map );
630
631
		$this->checkProducts( $map );
632
633
		$old = $this->products;
634
		$this->products = is_map( $map ) ? $map->toArray() : $map;
635
		$this->setModified();
636
637
		$this->notify( 'setProducts.after', $old );
638
639
		return $this;
640
	}
641
642
643
	/**
644
	 * Adds an order service to the basket
645
	 *
646
	 * @param \Aimeos\MShop\Order\Item\Service\Iface $service Order service item for the given domain
647
	 * @param string $type Service type constant from \Aimeos\MShop\Order\Item\Service\Base
648
	 * @param int|null $position Position of the service in the list to overwrite
649
	 * @return \Aimeos\MShop\Order\Item\Iface Order base item for method chaining
650
	 */
651
	public function addService( \Aimeos\MShop\Order\Item\Service\Iface $service, string $type, int $position = null ) : \Aimeos\MShop\Order\Item\Iface
652
	{
653
		$service = $this->notify( 'addService.before', $service );
654
655
		$this->checkPrice( $service->getPrice() );
656
657
		$service = clone $service;
658
		$service = $service->setType( $type );
659
660
		if( $position !== null ) {
661
			$this->services[$type][$position] = $service;
662
		} else {
663
			$this->services[$type][] = $service;
664
		}
665
666
		$this->setModified();
667
668
		$this->notify( 'addService.after', $service );
669
670
		return $this;
671
	}
672
673
674
	/**
675
	 * Deletes an order service from the basket
676
	 *
677
	 * @param string $type Service type constant from \Aimeos\MShop\Order\Item\Service\Base
678
	 * @param int|null $position Position of the service in the list to delete
679
	 * @return \Aimeos\MShop\Order\Item\Iface Order base item for method chaining
680
	 */
681
	public function deleteService( string $type, int $position = null ) : \Aimeos\MShop\Order\Item\Iface
682
	{
683
		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...
684
		{
685
			$old = ( isset( $this->services[$type][$position] ) ? $this->services[$type][$position] : $this->services[$type] );
686
			$old = $this->notify( 'deleteService.before', $old );
687
688
			if( $position !== null ) {
689
				unset( $this->services[$type][$position] );
690
			} else {
691
				unset( $this->services[$type] );
692
			}
693
694
			$this->setModified();
695
696
			$this->notify( 'deleteService.after', $old );
697
		}
698
699
		return $this;
700
	}
701
702
703
	/**
704
	 * Returns the order services depending on the given type
705
	 *
706
	 * @param string $type Service type constant from \Aimeos\MShop\Order\Item\Service\Base
707
	 * @param int|null $position Position of the service in the list to retrieve
708
	 * @return \Aimeos\MShop\Order\Item\Service\Iface[]|\Aimeos\MShop\Order\Item\Service\Iface
709
	 * 	Order service item or list of items for the requested type
710
	 * @throws \Aimeos\MShop\Order\Exception If no service for the given type and position is found
711
	 */
712
	public function getService( string $type, int $position = null )
713
	{
714
		if( $position !== null )
715
		{
716
			if( isset( $this->services[$type][$position] ) ) {
717
				return $this->services[$type][$position];
718
			}
719
720
			throw new \Aimeos\MShop\Order\Exception( sprintf( 'Service not available' ) );
721
		}
722
723
		return ( isset( $this->services[$type] ) ? $this->services[$type] : [] );
724
	}
725
726
727
	/**
728
	 * Returns all services that are part of the basket
729
	 *
730
	 * @return \Aimeos\Map Associative list of service types ("delivery" or "payment") as keys and list of
731
	 *	service items implementing \Aimeos\MShop\Order\Service\Iface as values
732
	 */
733
	public function getServices() : \Aimeos\Map
734
	{
735
		return map( $this->services );
736
	}
737
738
739
	/**
740
	 * Replaces all services in the current basket with the new ones
741
	 *
742
	 * @param \Aimeos\MShop\Order\Item\Service\Iface[] $map Associative list of order services as returned by getServices()
743
	 * @return \Aimeos\MShop\Order\Item\Iface Order base item for method chaining
744
	 */
745
	public function setServices( iterable $map ) : \Aimeos\MShop\Order\Item\Iface
746
	{
747
		$map = $this->notify( 'setServices.before', $map );
748
749
		foreach( $map as $type => $services ) {
750
			$map[$type] = $this->checkServices( $services, $type );
751
		}
752
753
		$old = $this->services;
754
		$this->services = is_map( $map ) ? $map->toArray() : $map;
755
		$this->setModified();
756
757
		$this->notify( 'setServices.after', $old );
758
759
		return $this;
760
	}
761
762
763
	/**
764
	 * Returns the service costs
765
	 *
766
	 * @param string $type Service type like "delivery" or "payment"
767
	 * @return float Service costs value
768
	 */
769
	public function getCosts( string $type = 'delivery' ) : float
770
	{
771
		$costs = 0;
772
773
		if( $type === 'delivery' )
774
		{
775
			foreach( $this->getProducts() as $product ) {
776
				$costs += $product->getPrice()->getCosts() * $product->getQuantity();
777
			}
778
		}
779
780
		foreach( $this->getService( $type ) as $service ) {
781
			$costs += $service->getPrice()->getCosts();
782
		}
783
784
		return $costs;
785
	}
786
787
788
	/**
789
	 * Returns a list of tax names and values
790
	 *
791
	 * @return array Associative list of tax names as key and price items as value
792
	 */
793
	public function getTaxes() : array
794
	{
795
		$taxes = [];
796
797
		foreach( $this->getProducts() as $product )
798
		{
799
			$price = $product->getPrice();
800
801
			foreach( $price->getTaxrates() as $name => $taxrate )
802
			{
803
				$price = (clone $price)->setTaxRate( $taxrate );
804
805
				if( isset( $taxes[$name][$taxrate] ) ) {
806
					$taxes[$name][$taxrate]->addItem( $price, $product->getQuantity() );
807
				} else {
808
					$taxes[$name][$taxrate] = $price->addItem( $price, $product->getQuantity() - 1 );
809
				}
810
			}
811
		}
812
813
		foreach( $this->getServices() as $services )
814
		{
815
			foreach( $services as $service )
816
			{
817
				$price = $service->getPrice();
818
819
				foreach( $price->getTaxrates() as $name => $taxrate )
820
				{
821
					$price = (clone $price)->setTaxRate( $taxrate );
822
823
					if( isset( $taxes[$name][$taxrate] ) ) {
824
						$taxes[$name][$taxrate]->addItem( $price );
825
					} else {
826
						$taxes[$name][$taxrate] = $price;
827
					}
828
				}
829
			}
830
		}
831
832
		return $taxes;
833
	}
834
835
836
	/**
837
	 * Checks if the price uses the same currency as the price in the basket.
838
	 *
839
	 * @param \Aimeos\MShop\Price\Item\Iface $item Price item
840
	 */
841
	protected function checkPrice( \Aimeos\MShop\Price\Item\Iface $item )
842
	{
843
		$price = clone $this->price;
0 ignored issues
show
Bug Best Practice introduced by
The property price does not exist on Aimeos\MShop\Order\Item\Base. Since you implemented __get, consider adding a @property annotation.
Loading history...
844
		$price->addItem( $item );
845
	}
846
847
848
	/**
849
	 * Checks if the given delivery status is a valid constant.
850
	 *
851
	 * @param int $value Delivery status constant defined in \Aimeos\MShop\Order\Item\Base
852
	 * @return int Delivery status constant defined in \Aimeos\MShop\Order\Item\Base
853
	 * @throws \Aimeos\MShop\Order\Exception If delivery status is invalid
854
	 */
855
	protected function checkDeliveryStatus( int $value )
856
	{
857
		if( $value < \Aimeos\MShop\Order\Item\Base::STAT_UNFINISHED || $value > \Aimeos\MShop\Order\Item\Base::STAT_RETURNED ) {
858
			throw new \Aimeos\MShop\Order\Exception( sprintf( 'Order delivery status "%1$s" not within allowed range', $value ) );
859
		}
860
861
		return $value;
862
	}
863
864
865
	/**
866
	 * Checks the given payment status is a valid constant.
867
	 *
868
	 * @param int $value Payment status constant defined in \Aimeos\MShop\Order\Item\Base
869
	 * @return int Payment status constant defined in \Aimeos\MShop\Order\Item\Base
870
	 * @throws \Aimeos\MShop\Order\Exception If payment status is invalid
871
	 */
872
	protected function checkPaymentStatus( int $value )
873
	{
874
		if( $value < \Aimeos\MShop\Order\Item\Base::PAY_UNFINISHED || $value > \Aimeos\MShop\Order\Item\Base::PAY_RECEIVED ) {
875
			throw new \Aimeos\MShop\Order\Exception( sprintf( 'Order payment status "%1$s" not within allowed range', $value ) );
876
		}
877
878
		return $value;
879
	}
880
881
882
	/**
883
	 * Checks if all order addresses are valid
884
	 *
885
	 * @param \Aimeos\MShop\Order\Item\Address\Iface[] $items Order address items
886
	 * @param string $type Address type constant from \Aimeos\MShop\Order\Item\Address\Base
887
	 * @return \Aimeos\MShop\Order\Item\Address\Iface[] List of checked items
888
	 * @throws \Aimeos\MShop\Exception If one of the order addresses is invalid
889
	 */
890
	protected function checkAddresses( iterable $items, string $type ) : iterable
891
	{
892
		map( $items )->implements( \Aimeos\MShop\Order\Item\Address\Iface::class, true );
893
894
		foreach( $items as $key => $item ) {
895
			$items[$key] = $item->setType( $type );
896
		}
897
898
		return $items;
899
	}
900
901
902
	/**
903
	 * Checks if all order products are valid
904
	 *
905
	 * @param \Aimeos\MShop\Order\Item\Product\Iface[] $items Order product items
906
	 * @return \Aimeos\MShop\Order\Item\Product\Iface[] List of checked items
907
	 * @throws \Aimeos\MShop\Exception If one of the order products is invalid
908
	 */
909
	protected function checkProducts( iterable $items ) : \Aimeos\Map
910
	{
911
		map( $items )->implements( \Aimeos\MShop\Order\Item\Product\Iface::class, true );
912
913
		foreach( $items as $key => $item )
914
		{
915
			if( $item->getProductCode() === '' ) {
916
				throw new \Aimeos\MShop\Order\Exception( sprintf( 'Product does not contain the SKU code' ) );
917
			}
918
919
			$this->checkPrice( $item->getPrice() );
920
		}
921
922
		return map( $items );
923
	}
924
925
926
	/**
927
	 * Checks if all order services are valid
928
	 *
929
	 * @param \Aimeos\MShop\Order\Item\Service\Iface[] $items Order service items
930
	 * @param string $type Service type constant from \Aimeos\MShop\Order\Item\Service\Base
931
	 * @return \Aimeos\MShop\Order\Item\Service\Iface[] List of checked items
932
	 * @throws \Aimeos\MShop\Exception If one of the order services is invalid
933
	 */
934
	protected function checkServices( iterable $items, string $type ) : iterable
935
	{
936
		map( $items )->implements( \Aimeos\MShop\Order\Item\Service\Iface::class, true );
937
938
		foreach( $items as $key => $item )
939
		{
940
			$this->checkPrice( $item->getPrice() );
941
			$items[$key] = $item->setType( $type );
942
		}
943
944
		return $items;
945
	}
946
947
948
	/**
949
	 * Tests if the given product is similar to an existing one.
950
	 * Similarity is described by the equality of properties so the quantity of
951
	 * the existing product can be updated.
952
	 *
953
	 * @param \Aimeos\MShop\Order\Item\Product\Iface $item Order product item
954
	 * @param \Aimeos\MShop\Order\Item\Product\Iface[] $products List of order product items to check against
955
	 * @return int|null Positon of the same product in the product list of false if product is unique
956
	 * @throws \Aimeos\MShop\Order\Exception If no similar item was found
957
	 */
958
	protected function getSameProduct( \Aimeos\MShop\Order\Item\Product\Iface $item, iterable $products ) : ?int
959
	{
960
		$map = [];
961
		$count = 0;
962
963
		foreach( $item->getAttributeItems() as $attributeItem )
964
		{
965
			$key = md5( $attributeItem->getCode() . json_encode( $attributeItem->getValue() ) );
966
			$map[$key] = $attributeItem;
967
			$count++;
968
		}
969
970
		foreach( $products as $position => $product )
971
		{
972
			if( $product->compare( $item ) === false ) {
973
				continue;
974
			}
975
976
			$prodAttributes = $product->getAttributeItems();
977
978
			if( count( $prodAttributes ) !== $count ) {
979
				continue;
980
			}
981
982
			foreach( $prodAttributes as $attribute )
983
			{
984
				$key = md5( $attribute->getCode() . json_encode( $attribute->getValue() ) );
985
986
				if( isset( $map[$key] ) === false || $map[$key]->getQuantity() != $attribute->getQuantity() ) {
987
					continue 2; // jump to outer loop
988
				}
989
			}
990
991
			return $position;
992
		}
993
994
		return null;
995
	}
996
}
997