Passed
Push — master ( 936923...57abee )
by Aimeos
02:39
created

Standard   A

Complexity

Total Complexity 34

Size/Duplication

Total Lines 373
Duplicated Lines 0 %

Importance

Changes 18
Bugs 0 Features 0
Metric Value
eloc 126
c 18
b 0
f 0
dl 0
loc 373
rs 9.68
wmc 34

11 Methods

Rating   Name   Duplication   Size   Complexity  
A getName() 0 3 1
A getDescription() 0 3 1
B addBasketServices() 0 40 6
A createOrderInvoice() 0 9 1
A addBasketProducts() 0 15 4
A createContext() 0 26 2
A createOrderBase() 0 13 1
A addBasketCoupons() 0 31 5
A addBasketAddresses() 0 13 3
A createPayment() 0 11 2
C run() 0 100 8
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
81
		$search = $manager->createSearch( true );
82
		$expr = [
83
			$search->compare( '<=', 'subscription.datenext', $date ),
84
			$search->combine( '||', [
85
				$search->compare( '==', 'subscription.dateend', null ),
86
				$search->compare( '>', 'subscription.dateend', $date ),
87
			] ),
88
			$search->getConditions(),
89
		];
90
		$search->setConditions( $search->combine( '&&', $expr ) );
91
		$search->setSortations( [$search->sort( '+', 'subscription.id' )] );
92
93
		$start = 0;
94
95
		do
96
		{
97
			$search->setSlice( $start, 100 );
98
			$items = $manager->searchItems( $search );
99
100
			foreach( $items as $item )
101
			{
102
				try
103
				{
104
					foreach( $processors as $processor ) {
105
						$processor->renewBefore( $item );
106
					}
107
108
					$context = $this->createContext( $item->getOrderBaseId() );
109
					$newOrder = $this->createOrderBase( $context, $item );
110
					$newInvoice = $this->createOrderInvoice( $context, $newOrder );
111
112
					try
113
					{
114
						$this->createPayment( $context, $newOrder, $newInvoice );
115
116
						$interval = new \DateInterval( $item->getInterval() );
117
						$date = date_create()->add( $interval )->format( 'Y-m-d' );
118
119
						$item = $item->setDateNext( $date )->setPeriod( $item->getPeriod() + 1 )->setReason( null );
120
					}
121
					catch( \Exception $e )
122
					{
123
						$item->setReason( \Aimeos\MShop\Subscription\Item\Iface::REASON_PAYMENT );
124
125
						if( $end ) {
126
							$item->setDateEnd( date_create()->format( 'Y-m-d' ) );
127
						}
128
129
						throw $e;
130
					}
131
					finally // will be always executed, even if exception is rethrown in catch()
132
					{
133
						foreach( $processors as $processor ) {
134
							$processor->renewAfter( $item, $newInvoice );
135
						}
136
					}
137
				}
138
				catch( \Exception $e )
139
				{
140
					$str = sprintf( 'Unable to renew subscription with ID "%1$s": %2$s', $item->getId(), $e->getMessage() );
141
					$logger->log( $str . "\n" . $e->getTraceAsString(), \Aimeos\MW\Logger\Base::ERR, 'subscription' );
142
				}
143
144
				$manager->saveItem( $item );
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

144
				$manager->/** @scrutinizer ignore-call */ 
145
              saveItem( $item );

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