Commit e3bd9dcf authored by lisastreeter's avatar lisastreeter Committed by bojanz

Issue #3017080 by lisastreeter, bojanz: Create a service for sending emails to customers

parent 093a0101
......@@ -123,6 +123,21 @@ function commerce_get_entity_display($entity_type, $bundle, $display_context) {
return $display;
}
/**
* Implements hook_mail().
*
* Captures the outgoing mail and sets appropriate message body and headers.
*/
function commerce_mail($key, &$message, $params) {
if (isset($params['headers'])) {
$message['headers'] = array_merge($message['headers'], $params['headers']);
}
$message['from'] = $params['from'];
$message['subject'] = $params['subject'];
$message['body'][] = $params['body'];
}
/**
* Helper for providing entity theme suggestions.
*
......
......@@ -72,3 +72,7 @@ services:
plugin.manager.commerce_inline_form:
class: Drupal\commerce\InlineFormManager
arguments: ['@container.namespaces', '@cache.discovery', '@module_handler']
commerce.mail_handler:
class: Drupal\commerce\MailHandler
arguments: ['@entity_type.manager', '@language_manager', '@plugin.manager.mail', '@renderer', '@theme.manager', '@theme.initialization']
......@@ -185,21 +185,6 @@ function commerce_order_views_data_alter(array &$data) {
$data['commerce_order']['state']['filter']['id'] = 'state_machine_state';
}
/**
* Implements hook_mail().
*
* Captures the outgoing mail and sets appropriate message body and headers.
*/
function commerce_order_mail($key, &$message, $params) {
if (isset($params['headers'])) {
$message['headers'] = array_merge($message['headers'], $params['headers']);
}
$message['from'] = $params['from'];
$message['subject'] = $params['subject'];
$message['body'][] = $params['body'];
}
/**
* Implements hook_ENTITY_TYPE_access().
*
......
......@@ -53,7 +53,7 @@ services:
commerce_order.order_receipt_subscriber:
class: Drupal\commerce_order\EventSubscriber\OrderReceiptSubscriber
arguments: ['@entity_type.manager', '@language_manager', '@plugin.manager.mail', '@commerce_order.order_total_summary', '@renderer', '@theme.manager', '@theme.initialization']
arguments: ['@entity_type.manager', '@commerce.mail_handler', '@commerce_order.order_total_summary']
tags:
- { name: 'event_subscriber' }
......
......@@ -2,15 +2,10 @@
namespace Drupal\commerce_order\EventSubscriber;
use Drupal\commerce\MailHandlerInterface;
use Drupal\commerce_order\OrderTotalSummaryInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Mail\MailManagerInterface;
use Drupal\Core\Render\RenderContext;
use Drupal\Core\Render\Renderer;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\Theme\ThemeInitializationInterface;
use Drupal\Core\Theme\ThemeManagerInterface;
use Drupal\state_machine\Event\WorkflowTransitionEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
......@@ -22,88 +17,48 @@ class OrderReceiptSubscriber implements EventSubscriberInterface {
use StringTranslationTrait;
/**
* The order type entity storage.
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityStorageInterface
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $orderTypeStorage;
protected $entityTypeManager;
/**
* The order total summary.
*
* @var \Drupal\commerce_order\OrderTotalSummaryInterface
*/
protected $orderTotalSummary;
/**
* The entity view builder for profiles.
*
* @var \Drupal\profile\ProfileViewBuilder
*/
protected $profileViewBuilder;
/**
* The language manager.
*
* @var \Drupal\Core\Language\LanguageManagerInterface
*/
protected $languageManager;
/**
* The mail manager.
*
* @var \Drupal\Core\Mail\MailManagerInterface
*/
protected $mailManager;
/**
* The renderer.
*
* @var \Drupal\Core\Render\RendererInterface
*/
protected $renderer;
/**
* The theme manager.
* The mail handler.
*
* @var \Drupal\Core\Theme\ThemeManagerInterface
* @var \Drupal\commerce\MailHandlerInterface
*/
protected $themeManager;
protected $mailHandler;
/**
* The theme initialization.
* The order total summary.
*
* @var \Drupal\Core\Theme\ThemeInitializationInterface
* @var \Drupal\commerce_order\OrderTotalSummaryInterface
*/
protected $themeInitialization;
protected $orderTotalSummary;
/**
* Constructs a new OrderReceiptSubscriber object.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
* The language manager.
* @param \Drupal\Core\Mail\MailManagerInterface $mail_manager
* The mail manager.
* @param \Drupal\commerce\MailHandlerInterface $mail_handler
* The mail handler.
* @param \Drupal\commerce_order\OrderTotalSummaryInterface $order_total_summary
* The order total summary.
* @param \Drupal\Core\Render\Renderer $renderer
* The renderer.
* @param \Drupal\Core\Theme\ThemeManagerInterface $theme_manager
* The theme manager.
* @param \Drupal\Core\Theme\ThemeInitializationInterface $theme_initialization
* The theme initialization.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager, LanguageManagerInterface $language_manager, MailManagerInterface $mail_manager, OrderTotalSummaryInterface $order_total_summary, Renderer $renderer, ThemeManagerInterface $theme_manager, ThemeInitializationInterface $theme_initialization) {
$this->orderTypeStorage = $entity_type_manager->getStorage('commerce_order_type');
public function __construct(EntityTypeManagerInterface $entity_type_manager, MailHandlerInterface $mail_handler, OrderTotalSummaryInterface $order_total_summary) {
$this->entityTypeManager = $entity_type_manager;
$this->mailHandler = $mail_handler;
$this->orderTotalSummary = $order_total_summary;
$this->profileViewBuilder = $entity_type_manager->getViewBuilder('profile');
$this->languageManager = $language_manager;
$this->mailManager = $mail_manager;
$this->renderer = $renderer;
$this->themeManager = $theme_manager;
$this->themeInitialization = $theme_initialization;
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents() {
$events = ['commerce_order.place.post_transition' => ['sendOrderReceipt', -100]];
return $events;
}
/**
......@@ -115,8 +70,9 @@ class OrderReceiptSubscriber implements EventSubscriberInterface {
public function sendOrderReceipt(WorkflowTransitionEvent $event) {
/** @var \Drupal\commerce_order\Entity\OrderInterface $order */
$order = $event->getEntity();
$order_type_storage = $this->entityTypeManager->getStorage('commerce_order_type');
/** @var \Drupal\commerce_order\Entity\OrderTypeInterface $order_type */
$order_type = $this->orderTypeStorage->load($order->bundle());
$order_type = $order_type_storage->load($order->bundle());
if (!$order_type->shouldSendReceipt()) {
return;
}
......@@ -126,71 +82,24 @@ class OrderReceiptSubscriber implements EventSubscriberInterface {
return;
}
$subject = $this->t('Order #@number confirmed', ['@number' => $order->getOrderNumber()]);
$body = [
'#theme' => 'commerce_order_receipt',
'#order_entity' => $order,
'#totals' => $this->orderTotalSummary->buildTotals($order),
];
if ($billing_profile = $order->getBillingProfile()) {
$profile_view_builder = $this->entityTypeManager->getViewBuilder('profile');;
$body['#billing_information'] = $profile_view_builder->view($billing_profile);
}
$params = [
'headers' => [
'Content-Type' => 'text/html; charset=UTF-8;',
'Content-Transfer-Encoding' => '8Bit',
],
'id' => 'order_receipt',
'to' => $to,
'from' => $order->getStore()->getEmail(),
'subject' => $this->t('Order #@number confirmed', ['@number' => $order->getOrderNumber()]),
'bcc' => $order_type->getReceiptBcc(),
'order' => $order,
];
if ($receipt_bcc = $order_type->getReceiptBcc()) {
$params['headers']['Bcc'] = $receipt_bcc;
}
// Switch the theme to the configured mail theme.
$mail_theme = NULL;
$current_active_theme = $this->themeManager->getActiveTheme();
// The Mail System module swaps out core's MailManager, adding a
// getMailTheme() method. However, this method is not on any interface.
if (method_exists($this->mailManager, 'getMailTheme')) {
$mail_theme = $this->mailManager->getMailTheme();
if ($mail_theme != $current_active_theme->getName()) {
$initialized_mail_theme = $this->themeInitialization->initTheme($mail_theme);
$this->themeManager->setActiveTheme($initialized_mail_theme);
}
}
try {
$build = [
'#theme' => 'commerce_order_receipt',
'#order_entity' => $order,
'#totals' => $this->orderTotalSummary->buildTotals($order),
];
if ($billing_profile = $order->getBillingProfile()) {
$build['#billing_information'] = $this->profileViewBuilder->view($billing_profile);
}
$params['body'] = $this->renderer->executeInRenderContext(new RenderContext(), function () use ($build) {
return $this->renderer->render($build);
});
}
finally {
// Revert the active theme.
if ($mail_theme != $current_active_theme->getName()) {
$this->themeManager->setActiveTheme($current_active_theme);
}
}
// Replicated logic from EmailAction and contact's MailHandler.
$customer = $order->getCustomer();
if ($customer->isAuthenticated()) {
$langcode = $customer->getPreferredLangcode();
}
else {
$langcode = $this->languageManager->getDefaultLanguage()->getId();
}
$this->mailManager->mail('commerce_order', 'receipt', $to, $langcode, $params);
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents() {
$events = ['commerce_order.place.post_transition' => ['sendOrderReceipt', -100]];
return $events;
$this->mailHandler->sendEmail($order->getCustomer(), $subject, $body, $params);
}
}
......@@ -109,14 +109,15 @@ class OrderReceiptTest extends CommerceKernelTestBase {
$this->order->getState()->applyTransitionById('place');
$this->order->save();
$mails = $this->getMails();
$this->assertEquals(1, count($mails));
$the_email = reset($mails);
$this->assertEquals('text/html; charset=UTF-8;', $the_email['headers']['Content-Type']);
$this->assertEquals('8Bit', $the_email['headers']['Content-Transfer-Encoding']);
$this->assertEquals('Order #2017/01 confirmed', $the_email['subject']);
$this->assertEmpty(isset($the_email['headers']['Bcc']));
$emails = $this->getMails();
$this->assertCount(1, $emails);
$email = reset($emails);
$this->assertEquals('text/html; charset=UTF-8;', $email['headers']['Content-Type']);
$this->assertEquals('8Bit', $email['headers']['Content-Transfer-Encoding']);
$this->assertEquals('Order #2017/01 confirmed', $email['subject']);
$this->assertContains('Thank you for your order!', $email['body']);
$this->assertFalse(isset($the_email['headers']['Bcc']));
}
/**
......@@ -130,8 +131,7 @@ class OrderReceiptTest extends CommerceKernelTestBase {
$this->order->getState()->applyTransitionById('place');
$this->order->save();
$mails = $this->getMails();
$this->assertEquals(0, count($mails));
$this->assertCount(0, $this->getMails());
}
/**
......@@ -145,11 +145,11 @@ class OrderReceiptTest extends CommerceKernelTestBase {
$this->order->getState()->applyTransitionById('place');
$this->order->save();
$mails = $this->getMails();
$this->assertEquals(1, count($mails));
$emails = $this->getMails();
$this->assertCount(1, $emails);
$the_email = reset($mails);
$this->assertEquals('bcc@example.com', $the_email['headers']['Bcc']);
$email = reset($emails);
$this->assertEquals('bcc@example.com', $email['headers']['Bcc']);
}
}
<?php
namespace Drupal\Tests\commerce_order\Kernel;
use Drupal\commerce_order\Entity\Order;
use Drupal\commerce_price\Price;
use Drupal\commerce_product\Entity\Product;
use Drupal\commerce_product\Entity\ProductVariation;
use Drupal\commerce_product\Entity\ProductVariationType;
use Drupal\Core\Test\AssertMailTrait;
use Drupal\profile\Entity\Profile;
use Drupal\Tests\commerce\Kernel\CommerceKernelTestBase;
/**
* Tests sending of order receipt emails using MailSystem mail theme setting.
*
* @requires module mailsystem
* @group commerce
*/
class OrderReceiptThemeTest extends CommerceKernelTestBase {
use AssertMailTrait;
/**
* A sample order.
*
* @var \Drupal\commerce_order\Entity\OrderInterface
*/
protected $order;
/**
* Modules to enable.
*
* @var array
*/
public static $modules = [
'system',
'filter',
'entity_reference_revisions',
'profile',
'state_machine',
'commerce_product',
'commerce_order',
'mailsystem',
'mailsystem_test',
];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->installEntitySchema('profile');
$this->installEntitySchema('commerce_order');
$this->installEntitySchema('commerce_order_item');
$this->installEntitySchema('commerce_product');
$this->installEntitySchema('commerce_product_variation');
$this->installConfig(['commerce_product', 'commerce_order', 'mailsystem']);
$user = $this->createUser(['mail' => $this->randomString() . '@example.com']);
\Drupal::service('theme_handler')->install(['commerce_order_test_theme']);
// Turn off title generation to allow explicit values to be used.
$variation_type = ProductVariationType::load('default');
$variation_type->setGenerateTitle(FALSE);
$variation_type->save();
$product = Product::create([
'type' => 'default',
'title' => 'Default testing product',
]);
$product->save();
$variation1 = ProductVariation::create([
'type' => 'default',
'sku' => 'TEST_' . strtolower($this->randomMachineName()),
'title' => $this->randomString(),
'status' => 1,
'price' => new Price('12.00', 'USD'),
]);
$variation1->save();
$product->addVariation($variation1)->save();
$profile = Profile::create([
'type' => 'customer',
]);
$profile->save();
$profile = $this->reloadEntity($profile);
/** @var \Drupal\commerce_order\OrderItemStorageInterface $order_item_storage */
$order_item_storage = $this->container->get('entity_type.manager')->getStorage('commerce_order_item');
$order_item1 = $order_item_storage->createFromPurchasableEntity($variation1);
$order_item1->save();
$order = Order::create([
'type' => 'default',
'state' => 'draft',
'mail' => $user->getEmail(),
'uid' => $user->id(),
'ip_address' => '127.0.0.1',
'order_number' => '2017/01',
'billing_profile' => $profile,
'store_id' => $this->store->id(),
'order_items' => [$order_item1],
]);
$order->save();
$this->order = $this->reloadEntity($order);
}
/**
* Tests the order receipt without a custom theme.
*/
public function testOrderReceiptDefault() {
$mailsystem_config = $this->config('mailsystem.settings');
$mailsystem_config
->set('defaults.sender', 'test_mail_collector')
->set('defaults.formatter', 'test_mail_collector')
->save();
$this->order->getState()->applyTransitionById('place');
$this->order->save();
$mails = $this->getMails();
$this->assertEquals(1, count($mails));
$email = reset($mails);
$this->assertEquals('text/html; charset=UTF-8;', $email['headers']['Content-Type']);
$this->assertEquals('8Bit', $email['headers']['Content-Transfer-Encoding']);
$this->assertEquals('Order #2017/01 confirmed', $email['subject']);
$this->assertNotContains('Commerce order test theme', $email['body']);
}
/**
* Tests the order receipt with a custom theme.
*/
public function testOrderReceiptThemed() {
$mailsystem_config = $this->config('mailsystem.settings');
$mailsystem_config
->set('defaults.sender', 'test_mail_collector')
->set('defaults.formatter', 'test_mail_collector')
->set('theme', 'commerce_order_test_theme')
->save();
$this->order->getState()->applyTransitionById('place');
$this->order->save();
$mails = $this->getMails();
$this->assertEquals(1, count($mails));
$email = reset($mails);
$this->assertEquals('text/html; charset=UTF-8;', $email['headers']['Content-Type']);
$this->assertEquals('8Bit', $email['headers']['Content-Transfer-Encoding']);
$this->assertEquals('Order #2017/01 confirmed', $email['subject']);
$this->assertContains('Commerce order test theme', $email['body']);
$this->assertTrue(strpos($email['body'], 'Commerce order test theme') !== FALSE);
}
}
{#
/**
* @file
* Template for the order receipt.
*
* Available variables:
* - order_entity: The order entity.
* - billing_information: The billing information.
* - shipping_information: The shipping information.
* - payment_method: The payment method.
* - totals: An array of order totals values with the following keys:
* - subtotal: The order subtotal price.
* - adjustments: An array of adjustment totals:
* - type: The adjustment type.
* - label: The adjustment label.
* - total: The adjustment total price.
* - weight: The adjustment weight, taken from the adjustment type.
* - total: The order total price.
*
* @ingroup themeable
*/
#}
<p>Commerce order test theme</p>
<table style="margin: 15px auto 0 auto; max-width: 768px; font-family: arial,sans-serif">
<tbody>
<tr>
<td>
<table style="margin-left: auto; margin-right: auto; max-width: 768px; text-align: center;">
<tbody>
<tr>
<td>
<a href="{{ url('<front>') }}" style="color: #0e69be; text-decoration: none; font-weight: bold; margin-top: 15px;">{{ order_entity.getStore.label }}</a>
</td>
</tr>
</tbody>
</table>
<table style="text-align: center; min-width: 450px; margin: 5px auto 0 auto; border: 1px solid #cccccc; border-radius: 5px; padding: 40px 30px 30px 30px;">
<tbody>
<tr>
<td style="font-size: 30px; padding-bottom: 30px">{{ 'Order Confirmation'|t }}</td>
</tr>
<tr>
<td style="font-weight: bold; padding-top:15px; padding-bottom: 15px; text-align: left; border-top: 1px solid #cccccc; border-bottom: 1px solid #cccccc">
{{ 'Order #@number details:'|t({'@number': order_entity.getOrderNumber}) }}
</td>
</tr>
<tr>
<td>
{% block order_items %}
<table style="padding-top: 15px; padding-bottom:15px; width: 100%">
<tbody style="text-align: left;">
{% for order_item in order_entity.getItems %}
<tr>
<td>
{{ order_item.getQuantity|number_format }} x
</td>
<td>
<span>{{ order_item.label }}</span>
<span style="float: right;">{{ order_item.getTotalPrice|commerce_price_format }}</span>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}
</td>
</tr>
<tr>
<td>
{% if (billing_information or shipping_information) %}
<table style="width: 100%; padding-top:15px; padding-bottom: 15px; text-align: left; border-top: 1px solid #cccccc; border-bottom: 1px solid #cccccc">
<tbody>
<tr>
{% if shipping_information %}
<td style="padding-top: 5px; font-weight: bold;">{{ 'Shipping Information'|t }}</td>
{% endif %}
{% if billing_information %}
<td style="padding-top: 5px; font-weight: bold;">{{ 'Billing Information'|t }}</td>
{% endif %}
</tr>
<tr>
{% if shipping_information %}
<td>
{% block shipping_information %}
{{ shipping_information }}
{% endblock %}
</td>
{% endif %}
{% if billing_information %}
<td>
{% block billing_information %}
{{ billing_information }}
{% endblock %}
</td>
{% endif %}
</tr>
{% if payment_method %}
<tr>
<td style="font-weight: bold; margin-top: 10px;">{{ 'Payment Method'|t }}</td>
</tr>
<tr>
<td>
{% block payment_method %}
{{ payment_method }}
{% endblock %}
</td>
</tr>
{% endif %}
</tbody>
</table>
{% endif %}
</td>
</tr>
<tr>
<td>
<p style="margin-bottom: 0;">
{{ 'Subtotal: @subtotal'|t({'@subtotal': totals.subtotal|commerce_price_format}) }}
</p>
</td>
</tr>
{% for adjustment in totals.adjustments %}
<tr>
<td>
<p style="margin-bottom: 0;">
{{ adjustment.label }}: {{ adjustment.total|commerce_price_format }}
</p>
</td>
</tr>
{% endfor %}
<tr>
<td>
<p style="font-size: 24px; padding-top: 15px; padding-bottom: 5px;">
{{ 'Order Total: @total'|t({'@total': order_entity.getTotalPrice|commerce_price_format}) }}
</p>
</td>
</tr>
<tr>
<td>
{% block additional_information %}
{{ 'Thank you for your order!'|t }}
{% endblock %}
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<?php
namespace Drupal\commerce;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Mail\MailManagerInterface;
use Drupal\Core\Render\RenderContext;
use Drupal\Core\Render\Renderer;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\Theme\ThemeInitializationInterface;
use Drupal\Core\Theme\ThemeManagerInterface;
use Drupal\user\UserInterface;
class MailHandler implements MailHandlerInterface {
use StringTranslationTrait;
/**
* The store storage.
*
* @var \Drupal\commerce_store\StoreStorageInterface
*/
protected $storeStorage;
/**
* The language manager.
*
* @var \Drupal\Core\Language\LanguageManagerInterface
*/
protected $languageManager;
/**
* The mail manager.
*
* @var \Drupal\Core\Mail\MailManagerInterface
*/
protected $mailManager;
/**
* The renderer.
*
* @var \Drupal\Core\Render\RendererInterface
*/
protected $renderer;
/**
* The theme manager.
*
* @var \Drupal\Core\Theme\ThemeManagerInterface
*/
protected $themeManager;
/**
* The theme initialization.
*
* @var \Drupal\Core\Theme\ThemeInitializationInterface
*/