Passed
Push — master ( 48eef7...a2ffc3 )
by Aimeos
05:06
created

Standard::createOrder()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 3
c 0
b 0
f 0
nc 1
nop 2
dl 0
loc 7
rs 10
1
<?php
2
3
/**
4
 * @license LGPLv3, http://opensource.org/licenses/LGPL-3.0
5
 * @copyright Aimeos (aimeos.org), 2018-2020
6
 * @package Controller
7
 * @subpackage Jobs
8
 */
9
10
11
namespace Aimeos\Controller\Jobs\Subscription\Process\Renew;
12
13
14
/**
15
 * Job controller for subscription processs renew.
16
 *
17
 * @package Controller
18
 * @subpackage Jobs
19
 */
20
class Standard
21
	extends \Aimeos\Controller\Jobs\Subscription\Process\Base
22
	implements \Aimeos\Controller\Jobs\Iface
23
{
24
	/**
25
	 * Returns the localized name of the job.
26
	 *
27
	 * @return string Name of the job
28
	 */
29
	public function getName() : string
30
	{
31
		return $this->getContext()->getI18n()->dt( 'controller/jobs', 'Subscription process renew' );
32
	}
33
34
35
	/**
36
	 * Returns the localized description of the job.
37
	 *
38
	 * @return string Description of the job
39
	 */
40
	public function getDescription() : string
41
	{
42
		return $this->getContext()->getI18n()->dt( 'controller/jobs', 'Renews subscriptions at next date' );
43
	}
44
45
46
	/**
47
	 * Executes the job.
48
	 *
49
	 * @throws \Aimeos\Controller\Jobs\Exception If an error occurs
50
	 */
51
	public function run()
52
	{
53
		$context = $this->getContext();
54
		$config = $context->getConfig();
55
		$logger = $context->getLogger();
56
57
		/** controller/common/subscription/process/payment-ends
58
		 * Subscriptions ends if payment couldn't be captured
59
		 *
60
		 * By default, a subscription ends automatically if the next payment couldn't
61
		 * be captured. When setting this configuration to FALSE, the subscription job
62
		 * controller will try to capture the payment at the next run again until the
63
		 * subscription is deactivated manually.
64
		 *
65
		 * @param bool TRUE if payment failures ends the subscriptions, FALSE if not
66
		 * @since 2019.10
67
		 * @category Developer
68
		 * @category User
69
		 * @see controller/common/subscription/process/processors
70
		 * @see controller/common/subscription/process/payment-days
71
		 * @see controller/common/subscription/process/payment-status
72
		 */
73
		$end = (bool) $config->get( 'controller/common/subscription/process/payment-ends', true );
74
75
		$names = (array) $config->get( 'controller/common/subscription/process/processors', [] );
76
77
		$date = date( 'Y-m-d' );
78
		$processors = $this->getProcessors( $names );
79
		$manager = \Aimeos\MShop::create( $context, 'subscription' );
80
		$baseManager = \Aimeos\MShop::create( $context, 'order/base' );
81
		$orderManager = \Aimeos\MShop::create( $context, 'order' );
82
83
		$search = $manager->createSearch( true );
84
		$expr = [
85
			$search->compare( '<=', 'subscription.datenext', $date ),
86
			$search->combine( '||', [
87
				$search->compare( '==', 'subscription.dateend', null ),
88
				$search->compare( '>', 'subscription.dateend', $date ),
89
			] ),
90
			$search->getConditions(),
91
		];
92
		$search->setConditions( $search->combine( '&&', $expr ) );
93
		$search->setSortations( [$search->sort( '+', 'subscription.id' )] );
94
95
		$start = 0;
96
97
		do
98
		{
99
			$search->setSlice( $start, 100 );
100
			$items = $manager->searchItems( $search );
101
102
			foreach( $items as $item )
103
			{
104
				try
105
				{
106
					$context = $this->createContext( $item->getOrderBaseId() );
107
					$newOrder = $this->createOrder( $context, $item );
108
109
					foreach( $processors as $processor ) {
110
						$processor->renewBefore( $item, $newOrder );
111
					}
112
113
					$basket = $baseManager->store( $newOrder->getBaseItem()->check() );
114
					$newOrder = $orderManager->saveItem( $newOrder->setBaseId( $basket->getId() ) );
0 ignored issues
show
Bug introduced by
The method saveItem() does not exist on Aimeos\MShop\Common\Manager\Iface. Did you maybe mean saveItems()? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

114
					/** @scrutinizer ignore-call */ 
115
     $newOrder = $orderManager->saveItem( $newOrder->setBaseId( $basket->getId() ) );

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
115
116
					try
117
					{
118
						$this->createPayment( $context, $basket, $newOrder );
119
120
						$interval = new \DateInterval( $item->getInterval() );
121
						$date = date_create( $item->getDateNext() )->add( $interval )->format( 'Y-m-d' );
122
123
						$item = $item->setDateNext( $date )->setPeriod( $item->getPeriod() + 1 )->setReason( null );
124
					}
125
					catch( \Exception $e )
126
					{
127
						$item->setReason( \Aimeos\MShop\Subscription\Item\Iface::REASON_PAYMENT );
128
129
						if( $end ) {
130
							$item->setDateEnd( date_create()->format( 'Y-m-d' ) );
131
						}
132
133
						throw $e;
134
					}
135
					finally // will be always executed, even if exception is rethrown in catch()
136
					{
137
						foreach( $processors as $processor ) {
138
							$processor->renewAfter( $item, $newOrder );
139
						}
140
					}
141
				}
142
				catch( \Exception $e )
143
				{
144
					$str = sprintf( 'Unable to renew subscription with ID "%1$s": %2$s', $item->getId(), $e->getMessage() );
145
					$logger->log( $str . "\n" . $e->getTraceAsString(), \Aimeos\MW\Logger\Base::ERR, 'subscription' );
146
				}
147
148
				$manager->saveItem( $item );
149
			}
150
151
			$count = count( $items );
152
			$start += $count;
153
		}
154
		while( $count === $search->getSliceSize() );
155
	}
156
157
158
	/**
159
	 * Adds the given addresses to the basket
160
	 *
161
	 * @param \Aimeos\MShop\Context\Item\Iface Context object
0 ignored issues
show
Bug introduced by
The type Aimeos\Controller\Jobs\S...n\Process\Renew\Context was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
162
	 * @param \Aimeos\MShop\Order\Item\Base\Iface $basket Basket object to add the addresses to
163
	 * @param \Aimeos\Map $addresses List of type as key and address object implementing \Aimeos\MShop\Order\Item\Base\Address\Iface as value
164
	 * @return \Aimeos\MShop\Order\Item\Base\Iface Order with addresses added
165
	 */
166
	protected function addBasketAddresses( \Aimeos\MShop\Context\Item\Iface $context,
167
		\Aimeos\MShop\Order\Item\Base\Iface $newBasket, \Aimeos\Map $addresses ) : \Aimeos\MShop\Order\Item\Base\Iface
168
	{
169
		foreach( $addresses as $type => $orderAddresses )
170
		{
171
			$idx = 0;
172
173
			foreach( $orderAddresses as $orderAddress ) {
174
				$newBasket->addAddress( $orderAddress->setId( null ), $type, $idx );
175
			}
176
		}
177
178
		return $newBasket;
179
	}
180
181
182
	/**
183
	 * Adds the given coupon codes to basket if enabled
184
	 *
185
	 * @param \Aimeos\MShop\Context\Item\Iface Context object
186
	 * @param \Aimeos\MShop\Order\Item\Base\Iface $basket Order including product and addresses
187
	 * @param \Aimeos\Map $codes List of coupon codes that should be added to the given basket
188
	 * @return \Aimeos\MShop\Order\Item\Base\Iface Basket, maybe with coupons added
189
	 */
190
	protected function addBasketCoupons( \Aimeos\MShop\Context\Item\Iface $context,
191
		\Aimeos\MShop\Order\Item\Base\Iface $basket, \Aimeos\Map $codes ) : \Aimeos\MShop\Order\Item\Base\Iface
192
	{
193
		/** controller/jobs/subcription/process/renew/standard/use-coupons
194
		 * Applies the coupons of the previous order also to the new one
195
		 *
196
		 * Reuse coupon codes added to the basket by the customer the first time
197
		 * again in new subcription orders. If they have any effect depends on
198
		 * the codes still being active (status, time frame and count) and the
199
		 * decorators added to the coupon providers in the admin interface.
200
		 *
201
		 * @param boolean True to reuse coupon codes, false to remove coupons
202
		 * @category Developer
203
		 * @category User
204
		 * @since 2018.10
205
		 */
206
		if( $context->getConfig()->get( 'controller/jobs/subcription/process/renew/standard/use-coupons', false ) )
207
		{
208
			foreach( $codes as $code )
209
			{
210
				try {
211
					$basket->addCoupon( $code );
212
				} catch( \Aimeos\MShop\Plugin\Provider\Exception | \Aimeos\MShop\Coupon\Exception $e ) {
213
					$basket->deleteCoupon( $code );
214
				}
215
			}
216
		}
217
218
		return $basket;
219
	}
220
221
222
	/**
223
	 * Adds the given products to the basket
224
	 *
225
	 * @param \Aimeos\MShop\Context\Item\Iface Context object
226
	 * @param \Aimeos\MShop\Order\Item\Base\Iface $basket Basket object to add the products to
227
	 * @param \Aimeos\Map $orderProducts List of product items Implementing \Aimeos\MShop\Order\Item\Base\Product\Iface
228
	 * @param string $orderProductId Unique ID of the ordered subscription product
229
	 * @return \Aimeos\MShop\Order\Item\Base\Iface Order with products added
230
	 */
231
	protected function addBasketProducts( \Aimeos\MShop\Context\Item\Iface $context,
232
		\Aimeos\MShop\Order\Item\Base\Iface $newBasket, \Aimeos\Map $orderProducts, $orderProductId ) : \Aimeos\MShop\Order\Item\Base\Iface
233
	{
234
		foreach( $orderProducts as $orderProduct )
235
		{
236
			if( $orderProduct->getId() == $orderProductId )
237
			{
238
				foreach( $orderProduct->getAttributeItems() as $attrItem ) {
239
					$attrItem->setId( null );
240
				}
241
				$newBasket->addProduct( $orderProduct->setId( null ) );
242
			}
243
		}
244
245
		return $newBasket;
246
	}
247
248
249
	/**
250
	 * Adds a matching delivery and payment service to the basket
251
	 *
252
	 * @param \Aimeos\MShop\Context\Item\Iface Context object
253
	 * @param \Aimeos\MShop\Order\Item\Base\Iface $basket Basket object to add the services to
254
	 * @param \Aimeos\Map $services Associative list of type as key and list of service objects implementing \Aimeos\MShop\Order\Item\Base\Service\Iface as values
255
	 * @return \Aimeos\MShop\Order\Item\Base\Iface Order with delivery and payment service added
256
	 */
257
	protected function addBasketServices( \Aimeos\MShop\Context\Item\Iface $context,
258
		\Aimeos\MShop\Order\Item\Base\Iface $newBasket, \Aimeos\Map $services ) : \Aimeos\MShop\Order\Item\Base\Iface
259
	{
260
		$type = \Aimeos\MShop\Order\Item\Base\Service\Base::TYPE_PAYMENT;
261
262
		if( isset( $services[$type] ) )
263
		{
264
			$idx = 0;
265
266
			foreach( $services[$type] as $orderService )
267
			{
268
				foreach( $orderService->getAttributeItems() as $attrItem ) {
269
					$attrItem->setId( null );
270
				}
271
				$newBasket->addService( $orderService->setId( null ), $type, $idx++ );
272
			}
273
		}
274
275
		$idx = 0;
276
		$type = \Aimeos\MShop\Order\Item\Base\Service\Base::TYPE_DELIVERY;
277
278
		$serviceManager = \Aimeos\MShop::create( $context, 'service' );
279
		$orderServiceManager = \Aimeos\MShop::create( $context, 'order/base/service' );
280
281
		$search = $serviceManager->createSearch( true );
282
		$search->setSortations( [$search->sort( '+', 'service.position' )] );
283
		$search->setConditions( $search->compare( '==', 'service.type', $type ) );
284
285
		foreach( $serviceManager->searchItems( $search, ['media', 'price', 'text'] ) as $item )
286
		{
287
			$provider = $serviceManager->getProvider( $item, $item->getType() );
288
289
			if( $provider->isAvailable( $newBasket ) === true )
290
			{
291
				$orderServiceItem = $orderServiceManager->createItem()->copyFrom( $item );
292
				return $newBasket->addService( $orderServiceItem, $type, $idx++ );
293
			}
294
		}
295
296
		return $newBasket;
297
	}
298
299
300
	/**
301
	 * Creates a new context based on the order and the customer the subscription belongs to
302
	 *
303
	 * @param string $baseId Unique order base ID
304
	 * @return \Aimeos\MShop\Context\Item\Iface New context object
305
	 * @todo 2021.01 Pass site and locale as parameters instead of $baseId
306
	 */
307
	protected function createContext( string $baseId ) : \Aimeos\MShop\Context\Item\Iface
308
	{
309
		$context = clone $this->getContext();
310
311
		$manager = \Aimeos\MShop::create( $context, 'order/base' );
312
		$baseItem = $manager->getItem( $baseId );
313
		$sitecode = $baseItem->getSiteCode();
314
315
		$locale = $baseItem->getLocale();
316
		$level = \Aimeos\MShop\Locale\Manager\Base::SITE_ALL;
317
318
		$manager = \Aimeos\MShop::create( $context, 'locale' );
319
		$locale = $manager->bootstrap( $sitecode, $locale->getLanguageId(), $locale->getCurrencyId(), false, $level );
320
321
		$context->setLocale( $locale );
322
323
		try
324
		{
325
			$manager = \Aimeos\MShop::create( $context, 'customer' );
326
			$customerItem = $manager->getItem( $baseItem->getCustomerId(), ['customer/group'] );
327
328
			$context->setUserId( $baseItem->getCustomerId() );
329
			$context->setGroupIds( $customerItem->getGroups() );
330
		}
331
		catch( \Exception $e ) {} // Subscription without account
332
333
		return $context;
334
	}
335
336
337
	/**
338
	 * Creates and stores a new invoice for the given order basket
339
	 *
340
	 * @param \Aimeos\MShop\Context\Item\Iface Context object
341
	 * @param \Aimeos\MShop\Order\Item\Base\Iface $basket Complete order with product, addresses and services saved to the storage
342
	 * @return \Aimeos\MShop\Order\Item\Iface New invoice item including the order base item
343
	 */
344
	protected function createOrder( \Aimeos\MShop\Context\Item\Iface $context,
345
		\Aimeos\MShop\Subscription\Item\Iface $subscription ) : \Aimeos\MShop\Order\Item\Iface
346
	{
347
		$manager = \Aimeos\MShop::create( $context, 'order' );
348
		$basket = $this->createOrderBase( $context, $subscription );
349
350
		return $manager->createItem()->setBaseItem( $basket )->setType( 'subscription' );
351
	}
352
353
354
	/**
355
	 * Creates and stores a new order for the subscribed product
356
	 *
357
	 * @param \Aimeos\MShop\Context\Item\Iface Context object
358
	 * @param \Aimeos\MShop\Subscription\Item\Iface $subscription Subscription item with order base ID and order product ID
359
	 * @return \Aimeos\MShop\Order\Item\Base\Iface Complete order with product, addresses and services saved to the storage
360
	 */
361
	protected function createOrderBase( \Aimeos\MShop\Context\Item\Iface $context, \Aimeos\MShop\Subscription\Item\Iface $subscription ) : \Aimeos\MShop\Order\Item\Base\Iface
362
	{
363
		$manager = \Aimeos\MShop::create( $context, 'order/base' );
364
365
		$basket = $manager->load( $subscription->getOrderBaseId() );
366
		$newBasket = $manager->createItem()->setCustomerId( $basket->getCustomerId() );
367
368
		$newBasket = $this->addBasketAddresses( $context, $newBasket, $basket->getAddresses() );
369
		$newBasket = $this->addBasketProducts( $context, $newBasket, $basket->getProducts(), $subscription->getOrderProductId() );
370
		$newBasket = $this->addBasketServices( $context, $newBasket, $basket->getServices() );
371
		$newBasket = $this->addBasketCoupons( $context, $newBasket, $basket->getCoupons()->keys() );
372
373
		return $newBasket->check();
374
	}
375
376
377
	/**
378
	 * Creates and stores a new invoice for the given order basket
379
	 *
380
	 * @param \Aimeos\MShop\Context\Item\Iface Context object
381
	 * @param \Aimeos\MShop\Order\Item\Base\Iface $basket Complete order with product, addresses and services saved to the storage
382
	 * @return \Aimeos\MShop\Order\Item\Iface New invoice item associated to the order saved to the storage
383
	 * @deprecated Use createOrder() instead
384
	 */
385
	protected function createOrderInvoice( \Aimeos\MShop\Context\Item\Iface $context, \Aimeos\MShop\Order\Item\Base\Iface $basket ) : \Aimeos\MShop\Order\Item\Iface
386
	{
387
		$manager = \Aimeos\MShop::create( $context, 'order' );
388
		$item = $manager->createItem()->setBaseItem( $basket )->setBaseId( $basket->getId() )->setType( 'subscription' );
389
390
		return $manager->saveItem( $item );
391
	}
392
393
394
	/**
395
	 * Creates a new payment for the given order and invoice
396
	 *
397
	 * @param \Aimeos\MShop\Context\Item\Iface Context object
398
	 * @param \Aimeos\MShop\Order\Item\Base\Iface $basket Complete order with product, addresses and services
399
	 * @param \Aimeos\MShop\Order\Item\Iface New invoice item associated to the order
0 ignored issues
show
Bug introduced by
The type Aimeos\Controller\Jobs\S...ption\Process\Renew\New was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
400
	 * @deprecated 2021.01 $basket will be removed, use $invoice->getBaseItem() instead
401
	 */
402
	protected function createPayment( \Aimeos\MShop\Context\Item\Iface $context, \Aimeos\MShop\Order\Item\Base\Iface $basket,
403
		\Aimeos\MShop\Order\Item\Iface $invoice )
404
	{
405
		$manager = \Aimeos\MShop::create( $context, 'service' );
406
407
		foreach( $basket->getService( \Aimeos\MShop\Order\Item\Base\Service\Base::TYPE_PAYMENT ) as $service ) {
408
			$manager->getProvider( $manager->getItem( $service->getServiceId() ), 'payment' )->repay( $invoice );
409
		}
410
	}
411
}
412