Passed
Push — master ( 694c78...f1dcbc )
by Aimeos
06:27 queued 02:01
created

Standard::view()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 18
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 11
nc 1
nop 4
dl 0
loc 18
rs 9.9
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * @license LGPLv3, http://opensource.org/licenses/LGPL-3.0
5
 * @copyright Aimeos (aimeos.org), 2018-2021
6
 * @package Controller
7
 * @subpackage Order
8
 */
9
10
11
namespace Aimeos\Controller\Jobs\Order\Email\Voucher;
12
13
use \Aimeos\MW\Logger\Base as Log;
14
15
16
/**
17
 * Order voucher e-mail job controller.
18
 *
19
 * @package Controller
20
 * @subpackage Order
21
 */
22
class Standard
23
	extends \Aimeos\Controller\Jobs\Base
24
	implements \Aimeos\Controller\Jobs\Iface
25
{
26
	private $couponId;
27
28
29
	/**
30
	 * Returns the localized name of the job.
31
	 *
32
	 * @return string Name of the job
33
	 */
34
	public function getName() : string
35
	{
36
		return $this->getContext()->translate( 'controller/jobs', 'Voucher related e-mails' );
37
	}
38
39
40
	/**
41
	 * Returns the localized description of the job.
42
	 *
43
	 * @return string Description of the job
44
	 */
45
	public function getDescription() : string
46
	{
47
		return $this->getContext()->translate( 'controller/jobs', 'Sends the e-mail with the voucher to the customer' );
48
	}
49
50
51
	/**
52
	 * Executes the job.
53
	 *
54
	 * @throws \Aimeos\Controller\Jobs\Exception If an error occurs
55
	 */
56
	public function run()
57
	{
58
		$context = $this->getContext();
59
		$config = $context->getConfig();
60
61
		/** controller/jobs/order/email/voucher/limit-days
62
		 * Only send voucher e-mails of orders that were created in the past within the configured number of days
63
		 *
64
		 * The voucher e-mails are normally send immediately after the voucher
65
		 * has been ordered. This option prevents e-mails for old orders from
66
		 * being send in case anything went wrong or an update failed to avoid
67
		 * confusion of customers.
68
		 *
69
		 * @param integer Number of days
70
		 * @since 2018.07
71
		 * @category User
72
		 * @category Developer
73
		 * @see controller/jobs/order/email/voucher/status
74
		 */
75
		$limit = $config->get( 'controller/jobs/order/email/voucher/limit-days', 30 );
76
		$limitDate = date( 'Y-m-d H:i:s', time() - $limit * 86400 );
77
78
		/** controller/jobs/order/email/voucher/status
79
		 * Only send e-mails containing voucher for these payment status values
80
		 *
81
		 * E-mail containing vouchers can be sent for these payment status values:
82
		 *
83
		 * * 0: deleted
84
		 * * 1: canceled
85
		 * * 2: refused
86
		 * * 3: refund
87
		 * * 4: pending
88
		 * * 5: authorized
89
		 * * 6: received
90
		 *
91
		 * @param integer Payment status constant
92
		 * @since 2018.07
93
		 * @category User
94
		 * @category Developer
95
		 * @see controller/jobs/order/email/voucher/limit-days
96
		 */
97
		$status = (array) $config->get( 'controller/jobs/order/email/voucher/status', \Aimeos\MShop\Order\Item\Base::PAY_RECEIVED );
98
99
100
		$client = \Aimeos\Client\Html\Email\Voucher\Factory::create( $context );
101
		$orderManager = \Aimeos\MShop::create( $context, 'order' );
102
103
		$orderSearch = $orderManager->filter();
104
105
		$param = array( \Aimeos\MShop\Order\Item\Status\Base::EMAIL_VOUCHER, '1' );
106
		$orderFunc = $orderSearch->make( 'order:status', $param );
107
108
		$expr = array(
109
			$orderSearch->compare( '>=', 'order.mtime', $limitDate ),
110
			$orderSearch->compare( '==', 'order.statuspayment', $status ),
111
			$orderSearch->compare( '==', 'order.base.product.type', 'voucher' ),
112
			$orderSearch->compare( '==', $orderFunc, 0 ),
113
		);
114
		$orderSearch->setConditions( $orderSearch->and( $expr ) );
115
116
		$start = 0;
117
118
		do
119
		{
120
			$items = $orderManager->search( $orderSearch );
121
122
			$this->process( $client, $items, 1 );
123
124
			$count = count( $items );
125
			$start += $count;
126
			$orderSearch->slice( $start );
127
		}
128
		while( $count >= $orderSearch->getLimit() );
129
	}
130
131
132
	/**
133
	 * Saves the given coupon codes
134
	 *
135
	 * @param array $map Associative list of coupon codes as keys and reference Ids as values
136
	 */
137
	protected function addCouponCodes( array $map )
138
	{
139
		$couponId = $this->getCouponId();
140
		$manager = \Aimeos\MShop::create( $this->getContext(), 'coupon/code' );
141
142
		foreach( $map as $code => $ref )
143
		{
144
			$item = $manager->create()->setParentId( $couponId )
145
				->setCode( $code )->setRef( $ref )->setCount( null ); // unlimited
146
147
			$manager->save( $item );
148
		}
149
	}
150
151
152
	/**
153
	 * Adds the status of the delivered e-mail for the given order ID
154
	 *
155
	 * @param string $orderId Unique order ID
156
	 * @param int $value Status value
157
	 */
158
	protected function addOrderStatus( string $orderId, int $value )
159
	{
160
		$orderStatusManager = \Aimeos\MShop::create( $this->getContext(), 'order/status' );
161
162
		$statusItem = $orderStatusManager->create()->setParentId( $orderId )->setValue( $value )
163
			->setType( \Aimeos\MShop\Order\Item\Status\Base::EMAIL_VOUCHER );
164
165
		$orderStatusManager->save( $statusItem );
166
	}
167
168
169
	/**
170
	 * Returns the delivery address item of the order
171
	 *
172
	 * @param \Aimeos\MShop\Order\Item\Base\Iface $orderBaseItem Order including address items
173
	 * @return \Aimeos\MShop\Order\Item\Base\Address\Iface Delivery or voucher address item
174
	 * @throws \Aimeos\Controller\Jobs\Exception If no address item is available
175
	 */
176
	protected function getAddressItem( \Aimeos\MShop\Order\Item\Base\Iface $orderBaseItem ) : \Aimeos\MShop\Order\Item\Base\Address\Iface
177
	{
178
		$type = \Aimeos\MShop\Order\Item\Base\Address\Base::TYPE_DELIVERY;
179
		if( ( $addr = current( $orderBaseItem->getAddress( $type ) ) ) !== false && $addr->getEmail() !== '' ) {
180
			return $addr;
181
		}
182
183
		$type = \Aimeos\MShop\Order\Item\Base\Address\Base::TYPE_PAYMENT;
184
		if( ( $addr = current( $orderBaseItem->getAddress( $type ) ) ) !== false ) {
185
			return $addr;
186
		}
187
188
		$msg = sprintf( 'No address found in order base with ID "%1$s"', $orderBaseItem->getId() );
189
		throw new \Aimeos\Controller\Jobs\Exception( $msg );
190
	}
191
192
193
	/**
194
	 * Returns the coupon ID for the voucher coupon
195
	 *
196
	 * @return string Unique ID of the coupon item
197
	 */
198
	protected function getCouponId() : string
199
	{
200
		if( !isset( $this->couponId ) )
201
		{
202
			$manager = \Aimeos\MShop::create( $this->getContext(), 'coupon' );
203
204
			$search = $manager->filter()->slice( 0, 1 );
205
			$search->setConditions( $search->compare( '=~', 'coupon.provider', 'Voucher' ) );
206
207
			if( ( $item = $manager->search( $search )->first() ) === null ) {
208
				throw new \Aimeos\Controller\Jobs\Exception( 'No coupon provider "Voucher" available' );
209
			}
210
211
			$this->couponId = $item->getId();
212
		}
213
214
		return $this->couponId;
215
	}
216
217
218
	/**
219
	 * Returns the ordered voucher products from the basket.
220
	 *
221
	 * @param \Aimeos\MShop\Order\Item\Base\Iface $orderBaseItem Basket object
222
	 * @return array List of order product items for the voucher products
223
	 */
224
	protected function getOrderProducts( \Aimeos\MShop\Order\Item\Base\Iface $orderBaseItem )
225
	{
226
		$list = [];
227
228
		foreach( $orderBaseItem->getProducts() as $orderProductItem )
229
		{
230
			if( $orderProductItem->getType() === 'voucher' ) {
231
				$list[] = $orderProductItem;
232
			}
233
234
			foreach( $orderProductItem->getProducts() as $subProductItem )
235
			{
236
				if( $subProductItem->getType() === 'voucher' ) {
237
					$list[] = $subProductItem;
238
				}
239
			}
240
		}
241
242
		return $list;
243
	}
244
245
246
	/**
247
	 * Returns an initialized view object
248
	 *
249
	 * @param \Aimeos\MShop\Context\Item\Iface $context Context item
250
	 * @param string $site Site code
251
	 * @param string|null $currencyId Three letter ISO currency code
252
	 * @param string|null $langId ISO language code, maybe country specific
253
	 * @return \Aimeos\MW\View\Iface Initialized view object
254
	 */
255
	protected function view( \Aimeos\MShop\Context\Item\Iface $context, string $site, string $currencyId = null, string $langId = null ) : \Aimeos\MW\View\Iface
256
	{
257
		$view = $context->view();
258
		$params = ['locale' => $langId, 'site' => $site, 'currency' => $currencyId];
259
260
		$helper = new \Aimeos\MW\View\Helper\Param\Standard( $view, $params );
261
		$view->addHelper( 'param', $helper );
262
263
		$helper = new \Aimeos\MW\View\Helper\Number\Locale( $view, $langId );
264
		$view->addHelper( 'number', $helper );
265
266
		$helper = new \Aimeos\MW\View\Helper\Config\Standard( $view, $context->getConfig() );
267
		$view->addHelper( 'config', $helper );
268
269
		$helper = new \Aimeos\MW\View\Helper\Translate\Standard( $view, $context->getI18n( $langId ) );
270
		$view->addHelper( 'translate', $helper );
271
272
		return $view;
273
	}
274
275
276
	/**
277
	 * Sends the voucher e-mail for the given orders
278
	 *
279
	 * @param \Aimeos\Client\Html\Iface $client HTML client object for rendering the voucher e-mails
280
	 * @param \Aimeos\Map $items List of order items implementing \Aimeos\MShop\Order\Item\Iface with their IDs as keys
281
	 * @param int $status Delivery status value
282
	 */
283
	protected function process( \Aimeos\Client\Html\Iface $client, \Aimeos\Map $items, int $status )
284
	{
285
		$context = $this->getContext();
286
		$couponManager = \Aimeos\MShop::create( $context, 'coupon' );
287
		$orderBaseManager = \Aimeos\MShop::create( $context, 'order/base' );
288
289
		foreach( $items as $id => $item )
290
		{
291
			$couponManager->begin();
292
			$orderBaseManager->begin();
293
294
			try
295
			{
296
				$orderBaseItem = $orderBaseManager->load( $item->getBaseId() )->off();
297
298
				$orderBaseItem = $this->createCoupons( $orderBaseItem );
299
				$orderBaseManager->store( $orderBaseItem );
300
301
				$this->addOrderStatus( $id, $status );
302
				$this->sendEmails( $orderBaseItem, $client );
303
304
				$orderBaseManager->commit();
305
				$couponManager->commit();
306
307
				$str = sprintf( 'Sent voucher e-mails for order ID "%1$s"', $item->getId() );
308
				$context->getLogger()->log( $str, Log::INFO, 'email/order/voucher' );
309
			}
310
			catch( \Exception $e )
311
			{
312
				$orderBaseManager->rollback();
313
				$couponManager->rollback();
314
315
				$str = 'Error while trying to send voucher e-mails for order ID "%1$s": %2$s';
316
				$msg = sprintf( $str, $item->getId(), $e->getMessage() . PHP_EOL . $e->getTraceAsString() );
317
				$context->getLogger()->log( $msg, Log::INFO, 'email/order/voucher' );
318
			}
319
		}
320
	}
321
322
323
	/**
324
	 * Creates coupon codes for the bought vouchers
325
	 *
326
	 * @param \Aimeos\MShop\Order\Item\Base\Iface $orderBaseItem Complete order including addresses, products, services
327
	 */
328
	protected function createCoupons( \Aimeos\MShop\Order\Item\Base\Iface $orderBaseItem )
329
	{
330
		$map = [];
331
		$manager = \Aimeos\MShop::create( $this->getContext(), 'order/base/product/attribute' );
332
333
		foreach( $this->getOrderProducts( $orderBaseItem ) as $orderProductItem )
334
		{
335
			if( $orderProductItem->getAttribute( 'coupon-code', 'coupon' ) === null )
336
			{
337
				$codes = [];
338
339
				for( $i = 0; $i < $orderProductItem->getQuantity(); $i++ )
340
				{
341
					$str = $i . getmypid() . microtime( true ) . $orderProductItem->getId();
342
					$code = substr( strtoupper( sha1( $str ) ), -8 );
343
					$map[$code] = $orderProductItem->getId();
344
					$codes[] = $code;
345
				}
346
347
				$item = $manager->create()->setCode( 'coupon-code' )->setType( 'coupon' )->setValue( $codes );
348
				$orderProductItem->setAttributeItem( $item );
349
			}
350
		}
351
352
		$this->addCouponCodes( $map );
353
		return $orderBaseItem;
354
	}
355
356
357
	/**
358
	 * Sends the voucher related e-mail for a single order
359
	 *
360
	 * @param \Aimeos\MShop\Order\Item\Base\Iface $orderBaseItem Complete order including addresses, products, services
361
	 * @param \Aimeos\Client\Html\Iface $client HTML client object for rendering the voucher e-mails
362
	 */
363
	protected function sendEmails( \Aimeos\MShop\Order\Item\Base\Iface $orderBaseItem, \Aimeos\Client\Html\Iface $client )
364
	{
365
		$context = $this->getContext();
366
		$addrItem = $this->getAddressItem( $orderBaseItem );
367
		$currencyId = $orderBaseItem->getPrice()->getCurrencyId();
368
		$langId = ( $addrItem->getLanguageId() ?: $orderBaseItem->getLocale()->getLanguageId() );
369
370
		$view = $this->view( $context, $orderBaseItem->getSiteCode(), $currencyId, $langId );
371
372
		foreach( $this->getOrderProducts( $orderBaseItem ) as $orderProductItem )
373
		{
374
			if( ( $codes = $orderProductItem->getAttribute( 'coupon-code', 'coupon' ) ) !== null )
375
			{
376
				foreach( (array) $codes as $code )
377
				{
378
					$message = $context->getMail()->createMessage();
379
					$view->addHelper( 'mail', new \Aimeos\MW\View\Helper\Mail\Standard( $view, $message ) );
380
381
					$view->extOrderProductItem = $orderProductItem;
382
					$view->extAddressItem = $addrItem;
383
					$view->extVoucherCode = $code;
384
385
					$client->setView( $view );
386
					$client->header();
387
					$client->body();
388
389
					$context->getMail()->send( $view->mail() );
390
				}
391
			}
392
		}
393
	}
394
}
395