Passed
Push — master ( a0f3a6...c7cfed )
by Aimeos
03:06
created

Standard::sites()

Size

Total Lines 12
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 6
nc 2
nop 1
dl 0
loc 12
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-2022
6
 * @package Controller
7
 * @subpackage Order
8
 */
9
10
11
namespace Aimeos\Controller\Jobs\Order\Email\Voucher;
12
13
14
/**
15
 * Order voucher e-mail job controller.
16
 *
17
 * @package Controller
18
 * @subpackage Order
19
 */
20
class Standard
21
	extends \Aimeos\Controller\Jobs\Base
22
	implements \Aimeos\Controller\Jobs\Iface
23
{
24
	/** controller/jobs/order/email/voucher/name
25
	 * Class name of the used order email voucher scheduler controller implementation
26
	 *
27
	 * Each default job controller can be replace by an alternative imlementation.
28
	 * To use this implementation, you have to set the last part of the class
29
	 * name as configuration value so the controller factory knows which class it
30
	 * has to instantiate.
31
	 *
32
	 * For example, if the name of the default class is
33
	 *
34
	 *  \Aimeos\Controller\Jobs\Order\Email\Voucher\Standard
35
	 *
36
	 * and you want to replace it with your own version named
37
	 *
38
	 *  \Aimeos\Controller\Jobs\Order\Email\Voucher\Myvoucher
39
	 *
40
	 * then you have to set the this configuration option:
41
	 *
42
	 *  controller/jobs/order/email/voucher/name = Myvoucher
43
	 *
44
	 * The value is the last part of your own class name and it's case sensitive,
45
	 * so take care that the configuration value is exactly named like the last
46
	 * part of the class name.
47
	 *
48
	 * The allowed characters of the class name are A-Z, a-z and 0-9. No other
49
	 * characters are possible! You should always start the last part of the class
50
	 * name with an upper case character and continue only with lower case characters
51
	 * or numbers. Avoid chamel case names like "MyVoucher"!
52
	 *
53
	 * @param string Last part of the class name
54
	 * @since 2014.03
55
	 * @category Developer
56
	 */
57
58
	/** controller/jobs/order/email/voucher/decorators/excludes
59
	 * Excludes decorators added by the "common" option from the order email voucher controllers
60
	 *
61
	 * Decorators extend the functionality of a class by adding new aspects
62
	 * (e.g. log what is currently done), executing the methods of the underlying
63
	 * class only in certain conditions (e.g. only for logged in users) or
64
	 * modify what is returned to the caller.
65
	 *
66
	 * This option allows you to remove a decorator added via
67
	 * "controller/jobs/common/decorators/default" before they are wrapped
68
	 * around the job controller.
69
	 *
70
	 *  controller/jobs/order/email/voucher/decorators/excludes = array( 'decorator1' )
71
	 *
72
	 * This would remove the decorator named "decorator1" from the list of
73
	 * common decorators ("\Aimeos\Controller\Jobs\Common\Decorator\*") added via
74
	 * "controller/jobs/common/decorators/default" to this job controller.
75
	 *
76
	 * @param array List of decorator names
77
	 * @since 2015.09
78
	 * @category Developer
79
	 * @see controller/jobs/common/decorators/default
80
	 * @see controller/jobs/order/email/voucher/decorators/global
81
	 * @see controller/jobs/order/email/voucher/decorators/local
82
	 */
83
84
	/** controller/jobs/order/email/voucher/decorators/global
85
	 * Adds a list of globally available decorators only to the order email voucher controllers
86
	 *
87
	 * Decorators extend the functionality of a class by adding new aspects
88
	 * (e.g. log what is currently done), executing the methods of the underlying
89
	 * class only in certain conditions (e.g. only for logged in users) or
90
	 * modify what is returned to the caller.
91
	 *
92
	 * This option allows you to wrap global decorators
93
	 * ("\Aimeos\Controller\Jobs\Common\Decorator\*") around the job controller.
94
	 *
95
	 *  controller/jobs/order/email/voucher/decorators/global = array( 'decorator1' )
96
	 *
97
	 * This would add the decorator named "decorator1" defined by
98
	 * "\Aimeos\Controller\Jobs\Common\Decorator\Decorator1" only to this job controller.
99
	 *
100
	 * @param array List of decorator names
101
	 * @since 2015.09
102
	 * @category Developer
103
	 * @see controller/jobs/common/decorators/default
104
	 * @see controller/jobs/order/email/voucher/decorators/excludes
105
	 * @see controller/jobs/order/email/voucher/decorators/local
106
	 */
107
108
	/** controller/jobs/order/email/voucher/decorators/local
109
	 * Adds a list of local decorators only to the order email voucher controllers
110
	 *
111
	 * Decorators extend the functionality of a class by adding new aspects
112
	 * (e.g. log what is currently done), executing the methods of the underlying
113
	 * class only in certain conditions (e.g. only for logged in users) or
114
	 * modify what is returned to the caller.
115
	 *
116
	 * This option allows you to wrap local decorators
117
	 * ("\Aimeos\Controller\Jobs\Order\Email\Voucher\Decorator\*") around this job controller.
118
	 *
119
	 *  controller/jobs/order/email/voucher/decorators/local = array( 'decorator2' )
120
	 *
121
	 * This would add the decorator named "decorator2" defined by
122
	 * "\Aimeos\Controller\Jobs\Order\Email\Voucher\Decorator\Decorator2" only to this job
123
	 * controller.
124
	 *
125
	 * @param array List of decorator names
126
	 * @since 2015.09
127
	 * @category Developer
128
	 * @see controller/jobs/common/decorators/default
129
	 * @see controller/jobs/order/email/voucher/decorators/excludes
130
	 * @see controller/jobs/order/email/voucher/decorators/global
131
	 */
132
133
134
	use \Aimeos\Controller\Jobs\Mail;
135
136
137
	private $couponId;
138
139
140
	/**
141
	 * Returns the localized name of the job.
142
	 *
143
	 * @return string Name of the job
144
	 */
145
	public function getName() : string
146
	{
147
		return $this->context()->translate( 'controller/jobs', 'Voucher related e-mails' );
148
	}
149
150
151
	/**
152
	 * Returns the localized description of the job.
153
	 *
154
	 * @return string Description of the job
155
	 */
156
	public function getDescription() : string
157
	{
158
		return $this->context()->translate( 'controller/jobs', 'Sends the e-mail with the voucher to the customer' );
159
	}
160
161
162
	/**
163
	 * Executes the job.
164
	 *
165
	 * @throws \Aimeos\Controller\Jobs\Exception If an error occurs
166
	 */
167
	public function run()
168
	{
169
		$context = $this->context();
170
		$config = $context->config();
171
172
		/** controller/jobs/order/email/voucher/limit-days
173
		 * Only send voucher e-mails of orders that were created in the past within the configured number of days
174
		 *
175
		 * The voucher e-mails are normally send immediately after the voucher
176
		 * has been ordered. This option prevents e-mails for old orders from
177
		 * being send in case anything went wrong or an update failed to avoid
178
		 * confusion of customers.
179
		 *
180
		 * @param integer Number of days
181
		 * @since 2018.07
182
		 * @category User
183
		 * @category Developer
184
		 * @see controller/jobs/order/email/voucher/status
185
		 */
186
		$limit = $config->get( 'controller/jobs/order/email/voucher/limit-days', 30 );
187
		$limitDate = date( 'Y-m-d H:i:s', time() - $limit * 86400 );
188
189
		/** controller/jobs/order/email/voucher/status
190
		 * Only send e-mails containing voucher for these payment status values
191
		 *
192
		 * E-mail containing vouchers can be sent for these payment status values:
193
		 *
194
		 * * 0: deleted
195
		 * * 1: canceled
196
		 * * 2: refused
197
		 * * 3: refund
198
		 * * 4: pending
199
		 * * 5: authorized
200
		 * * 6: received
201
		 *
202
		 * @param integer Payment status constant
203
		 * @since 2018.07
204
		 * @category User
205
		 * @category Developer
206
		 * @see controller/jobs/order/email/voucher/limit-days
207
		 */
208
		$status = $config->get( 'controller/jobs/order/email/voucher/status', \Aimeos\MShop\Order\Item\Base::PAY_RECEIVED );
209
210
211
		$manager = \Aimeos\MShop::create( $context, 'order' );
212
213
		$filter = $manager->filter();
214
		$func = $filter->make( 'order:status', [\Aimeos\MShop\Order\Item\Status\Base::EMAIL_VOUCHER, '1'] );
215
		$filter->add( $filter->and( [
216
			$filter->compare( '>=', 'order.mtime', $limitDate ),
217
			$filter->compare( '==', 'order.statuspayment', $status ),
218
			$filter->compare( '==', 'order.base.product.type', 'voucher' ),
219
			$filter->compare( '==', $func, 0 ),
220
		] ) );
221
222
		$start = 0;
223
224
		do
225
		{
226
			$items = $manager->search( $filter->slice( $start ), ['order/base', 'order/base/addres', 'order/base/product'] );
227
228
			$this->notify( $items );
229
230
			$count = count( $items );
231
			$start += $count;
232
		}
233
		while( $count >= $filter->getLimit() );
234
	}
235
236
237
	/**
238
	 * Returns the delivery address item of the order
239
	 *
240
	 * @param \Aimeos\MShop\Order\Item\Base\Iface $orderBaseItem Order including address items
241
	 * @return \Aimeos\MShop\Order\Item\Base\Address\Iface Delivery or voucher address item
242
	 * @throws \Aimeos\Controller\Jobs\Exception If no address item is available
243
	 */
244
	protected function address( \Aimeos\MShop\Order\Item\Base\Iface $orderBaseItem ) : \Aimeos\MShop\Order\Item\Base\Address\Iface
245
	{
246
		$type = \Aimeos\MShop\Order\Item\Base\Address\Base::TYPE_DELIVERY;
247
		if( ( $addr = current( $orderBaseItem->getAddress( $type ) ) ) !== false && $addr->getEmail() !== '' ) {
248
			return $addr;
249
		}
250
251
		$type = \Aimeos\MShop\Order\Item\Base\Address\Base::TYPE_PAYMENT;
252
		if( ( $addr = current( $orderBaseItem->getAddress( $type ) ) ) !== false && $addr->getEmail() !== '' ) {
253
			return $addr;
254
		}
255
256
		$msg = sprintf( 'No address with e-mail found in order base with ID "%1$s"', $orderBaseItem->getId() );
257
		throw new \Aimeos\Controller\Jobs\Exception( $msg );
258
	}
259
260
261
	/**
262
	 * Creates coupon codes for the bought vouchers
263
	 *
264
	 * @param \Aimeos\Map $orderProdItems Complete order including addresses, products, services
265
	 */
266
	protected function createCoupons( \Aimeos\Map $orderProdItems )
267
	{
268
		$map = [];
269
		$manager = \Aimeos\MShop::create( $this->context(), 'order/base/product/attribute' );
270
271
		foreach( $orderProdItems as $orderProductItem )
272
		{
273
			if( $orderProductItem->getAttribute( 'coupon-code', 'coupon' ) ) {
274
				continue;
275
			}
276
277
			$codes = [];
278
279
			for( $i = 0; $i < $orderProductItem->getQuantity(); $i++ )
280
			{
281
				$str = $i . getmypid() . microtime( true ) . $orderProductItem->getId();
282
				$code = substr( strtoupper( sha1( $str ) ), -8 );
283
				$map[$code] = $orderProductItem->getId();
284
				$codes[] = $code;
285
			}
286
287
			$item = $manager->create()->setCode( 'coupon-code' )->setType( 'coupon' )->setValue( $codes );
288
			$orderProductItem->setAttributeItem( $item );
289
		}
290
291
		$this->saveCoupons( $map );
292
		return $orderProdItems;
293
	}
294
295
296
	/**
297
	 * Returns the coupon ID for the voucher coupon
298
	 *
299
	 * @return string Unique ID of the coupon item
300
	 */
301
	protected function couponId() : string
302
	{
303
		if( !isset( $this->couponId ) )
304
		{
305
			$manager = \Aimeos\MShop::create( $this->context(), 'coupon' );
306
			$filter = $manager->filter()->add( 'coupon.provider', '=~', 'Voucher' )->slice( 0, 1 );
307
308
			if( ( $item = $manager->search( $filter )->first() ) === null ) {
309
				throw new \Aimeos\Controller\Jobs\Exception( 'No coupon provider "Voucher" available' );
310
			}
311
312
			$this->couponId = $item->getId();
313
		}
314
315
		return $this->couponId;
316
	}
317
318
319
	/**
320
	 * Returns the PDF file name
321
	 *
322
	 * @param string $code Voucher code
323
	 * @return string PDF file name
324
	 */
325
	protected function filename( string $code ) : string
326
	{
327
		return $this->context()->translate( 'controller/jobs', 'Voucher' ) . '-' . $code . '.pdf';
328
	}
329
330
331
	/**
332
	 * Sends the voucher e-mail for the given orders
333
	 *
334
	 * @param \Aimeos\Map $items List of order items implementing \Aimeos\MShop\Order\Item\Iface with their IDs as keys
335
	 */
336
	protected function notify( \Aimeos\Map $items )
337
	{
338
		$context = $this->context();
339
		$sites = $this->sites( $items->getBaseItem()->getSiteId()->unique() );
340
341
		$couponManager = \Aimeos\MShop::create( $context, 'coupon' );
342
		$orderProdManager = \Aimeos\MShop::create( $context, 'order/base/product' );
343
344
		foreach( $items as $id => $item )
345
		{
346
			$couponManager->begin();
347
			$orderProdManager->begin();
348
349
			try
350
			{
351
				$base = $item->getBaseItem();
352
				$orderProdManager->save( $this->createCoupons( $this->products( $base ) ) );
353
354
				$addr = $this->address( $base );
355
				$context->locale()->setLanguageId( $addr->getLanguageId() );
356
357
				$list = $sites->get( $base->getSiteId(), map() );
358
				$view = $this->view( $base, $list->getTheme()->filter()->last() );
359
360
				$this->send( $view, $this->products( $base ), $addr, $list->getLogo()->filter()->last() );
361
				$this->status( $id );
362
363
				$orderProdManager->commit();
364
				$couponManager->commit();
365
366
				$str = sprintf( 'Sent voucher e-mails for order ID "%1$s"', $item->getId() );
367
				$context->logger()->info( $str, 'email/order/voucher' );
368
			}
369
			catch( \Exception $e )
370
			{
371
				$orderProdManager->rollback();
372
				$couponManager->rollback();
373
374
				$str = 'Error while trying to send voucher e-mails for order ID "%1$s": %2$s';
375
				$msg = sprintf( $str, $item->getId(), $e->getMessage() . PHP_EOL . $e->getTraceAsString() );
376
				$context->logger()->info( $msg, 'email/order/voucher' );
377
			}
378
		}
379
	}
380
381
382
	/**
383
	 * Returns the generated PDF file for the order
384
	 *
385
	 * @param \Aimeos\Base\View\Iface $view View object with address and order item assigned
386
	 * @return string|null PDF content or NULL for no PDF file
387
	 */
388
	protected function pdf( \Aimeos\Base\View\Iface $view ) : ?string
389
	{
390
		$config = $this->context()->config();
391
392
		/** controller/jobs/order/email/voucher/pdf
393
		 * Enables attaching a PDF to the voucher e-mail
394
		 *
395
		 * The voucher PDF contains the same information like the HTML e-mail.
396
		 *
397
		 * @param bool TRUE to enable attaching the PDF, FALSE to skip the PDF
398
		 * @since 2022.10
399
		 */
400
		if( !$config->get( 'controller/jobs/order/email/voucher/pdf', true ) ) {
401
			return null;
402
		}
403
404
		$pdf = new class( PDF_PAGE_ORIENTATION, PDF_UNIT, PDF_PAGE_FORMAT, true, 'UTF-8', false ) extends \TCPDF {
405
			private $headerFcn;
406
			private $footerFcn;
407
408
			public function Footer() { return ( $fcn = $this->footerFcn ) ? $fcn( $this ) : null; }
409
			public function Header() { return ( $fcn = $this->headerFcn ) ? $fcn( $this ) : null; }
410
			public function setFooterFunction( \Closure $fcn ) { $this->footerFcn = $fcn; }
411
			public function setHeaderFunction( \Closure $fcn ) { $this->headerFcn = $fcn; }
412
		};
413
		$pdf->setCreator( PDF_CREATOR );
414
		$pdf->setAuthor( 'Aimeos' );
415
416
		// Generate HTML before creating first PDF page to include header added in template
417
		$template = $config->get( 'controller/jobs/order/email/voucher/template-pdf', 'order/email/voucher/pdf' );
418
		$content = $view->set( 'pdf', $pdf )->render( $template );
419
420
		$pdf->addPage();
421
		$pdf->writeHtml( $content );
422
		$pdf->lastPage();
423
424
		return $pdf->output( '', 'S' );
425
	}
426
427
428
	/**
429
	 * Returns the ordered voucher products from the basket.
430
	 *
431
	 * @param \Aimeos\MShop\Order\Item\Base\Iface $orderBaseItem Basket object
432
	 * @return \Aimeos\Map List of order product items for the voucher products
433
	 */
434
	protected function products( \Aimeos\MShop\Order\Item\Base\Iface $orderBaseItem ) : \Aimeos\Map
435
	{
436
		$list = [];
437
438
		foreach( $orderBaseItem->getProducts() as $orderProductItem )
439
		{
440
			if( $orderProductItem->getType() === 'voucher' ) {
441
				$list[] = $orderProductItem;
442
			}
443
444
			foreach( $orderProductItem->getProducts() as $subProductItem )
445
			{
446
				if( $subProductItem->getType() === 'voucher' ) {
447
					$list[] = $subProductItem;
448
				}
449
			}
450
		}
451
452
		return map( $list );
453
	}
454
455
456
	/**
457
	 * Saves the given coupon codes
458
	 *
459
	 * @param array $map Associative list of coupon codes as keys and reference Ids as values
460
	 */
461
	protected function saveCoupons( array $map )
462
	{
463
		$couponId = $this->couponId();
464
		$manager = \Aimeos\MShop::create( $this->context(), 'coupon/code' );
465
466
		foreach( $map as $code => $ref )
467
		{
468
			$item = $manager->create()->setParentId( $couponId )
469
				->setCode( $code )->setRef( $ref )->setCount( null ); // unlimited
470
471
			$manager->save( $item );
472
		}
473
	}
474
475
476
	/**
477
	 * Sends the voucher related e-mail for a single order
478
	 *
479
	 * @param \Aimeos\Base\View\Iface $view Populated view object
480
	 * @param \Aimeos\Map $orderProducts List of ordered voucher products
481
	 * @param \Aimeos\MShop\Common\Item\Address\Iface $address Address item
482
	 * @param string|null $logoPath Relative path to the logo in the fs-media file system
483
	 */
484
	protected function send( \Aimeos\Base\View\Iface $view, \Aimeos\Map $orderProducts,
485
		\Aimeos\MShop\Common\Item\Address\Iface $address, string $logoPath = null )
486
	{
487
		/** controller/jobs/order/email/voucher/template-html
488
		 * Relative path to the template for the HTML part of the voucher emails.
489
		 *
490
		 * The template file contains the HTML code and processing instructions
491
		 * to generate the result shown in the body of the frontend. The
492
		 * configuration string is the path to the template file relative
493
		 * to the templates directory (usually in controller/jobs/templates).
494
		 * You can overwrite the template file configuration in extensions and
495
		 * provide alternative templates.
496
		 *
497
		 * @param string Relative path to the template
498
		 * @since 2022.04
499
		 * @see controller/jobs/order/email/voucher/template-text
500
		 */
501
502
		/** controller/jobs/order/email/voucher/template-text
503
		 * Relative path to the template for the text part of the voucher emails.
504
		 *
505
		 * The template file contains the text and processing instructions
506
		 * to generate the result shown in the body of the frontend. The
507
		 * configuration string is the path to the template file relative
508
		 * to the templates directory (usually in controller/jobs/templates).
509
		 * You can overwrite the template file configuration in extensions and
510
		 * provide alternative templates.
511
		 *
512
		 * @param string Relative path to the template
513
		 * @since 2022.04
514
		 * @see controller/jobs/order/email/voucher/template-html
515
		 */
516
517
		$context = $this->context();
518
		$config = $context->config();
519
		$logo = $this->call( 'mailLogo', $logoPath );
520
521
		foreach( $orderProducts as $orderProductItem )
522
		{
523
			if( !empty( $codes = $orderProductItem->getAttribute( 'coupon-code', 'coupon' ) ) )
524
			{
525
				foreach( (array) $codes as $code )
526
				{
527
					$view->orderProductItem = $orderProductItem;
528
					$view->voucher = $code;
529
530
					$msg = $this->call( 'mailTo', $address );
531
					$view->logo = $msg->embed( $logo, basename( (string) $logoPath ) );
532
533
					$msg->subject( $context->translate( 'controller/jobs', 'Your voucher' ) )
534
						->html( $view->render( $config->get( 'controller/jobs/order/email/voucher/template-html', 'order/email/voucher/html' ) ) )
535
						->text( $view->render( $config->get( 'controller/jobs/order/email/voucher/template-text', 'order/email/voucher/text' ) ) )
536
						->attach( $this->pdf( $view ), $this->call( 'filename', $code ), 'application/pdf' )
537
						->send();
538
				}
539
			}
540
		}
541
	}
542
543
544
	/**
545
	 * Returns the site items for the given site codes
546
	 *
547
	 * @param iterable $siteIds List of site IDs
548
	 * @return \Aimeos\Map Site items with codes as keys
549
	 */
550
	protected function sites( iterable $siteIds ) : \Aimeos\Map
551
	{
552
		$map = [];
553
		$manager = \Aimeos\MShop::create( $this->context(), 'locale/site' );
554
555
		foreach( $siteIds as $siteId )
556
		{
557
			$list = explode( '.', trim( $siteId, '.' ) );
558
			$map[$siteId] = $manager->getPath( end( $list ) );
559
		}
560
561
		return map( $map );
562
	}
563
564
565
	/**
566
	 * Adds the status of the delivered e-mail for the given order ID
567
	 *
568
	 * @param string $orderId Unique order ID
569
	 */
570
	protected function status( string $orderId )
571
	{
572
		$orderStatusManager = \Aimeos\MShop::create( $this->context(), 'order/status' );
573
574
		$statusItem = $orderStatusManager->create()->setParentId( $orderId )->setValue( 1 )
575
			->setType( \Aimeos\MShop\Order\Item\Status\Base::EMAIL_VOUCHER );
576
577
		$orderStatusManager->save( $statusItem );
578
	}
579
580
581
	/**
582
	 * Returns the view populated with common data
583
	 *
584
	 * @param \Aimeos\MShop\Order\Item\Base\Iface $base Basket including addresses
585
	 * @param string|null $theme Theme name
586
	 * @return \Aimeos\Base\View\Iface View object
587
	 */
588
	protected function view( \Aimeos\MShop\Order\Item\Base\Iface $base, string $theme = null ) : \Aimeos\Base\View\Iface
589
	{
590
		$address = $this->address( $base );
591
		$langId = $address->getLanguageId() ?: $base->locale()->getLanguageId();
592
593
		$view = $this->call( 'mailView', $langId );
594
		$view->intro = $this->call( 'mailIntro', $address );
595
		$view->css = $this->call( 'mailCss', $theme );
596
		$view->address = $address;
597
		$view->urlparams = [
598
			'currency' => $base->getPrice()->getCurrencyId(),
599
			'site' => $base->getSiteCode(),
600
			'locale' => $langId,
601
		];
602
603
		return $view;
604
	}
605
}
606