Skip to content

Commit 46211d7

Browse files
Merge branch 'ACQE-8964' into ACQE-functional-deployment-version22
2 parents 1cf6e92 + 7d4b8ca commit 46211d7

File tree

1 file changed

+245
-0
lines changed

1 file changed

+245
-0
lines changed
Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
<?php
2+
/**
3+
* Copyright 2025 Adobe
4+
* All Rights Reserved.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Magento\Sales\Model\Order\Email\Sender;
9+
10+
use Magento\Catalog\Test\Fixture\Product as ProductFixture;
11+
use Magento\Checkout\Test\Fixture\PlaceOrder as PlaceOrderFixture;
12+
use Magento\Checkout\Test\Fixture\SetBillingAddress as SetBillingAddressFixture;
13+
use Magento\Checkout\Test\Fixture\SetDeliveryMethod as SetDeliveryMethodFixture;
14+
use Magento\Checkout\Test\Fixture\SetGuestEmail as SetGuestEmailFixture;
15+
use Magento\Checkout\Test\Fixture\SetPaymentMethod as SetPaymentMethodFixture;
16+
use Magento\Checkout\Test\Fixture\SetShippingAddress as SetShippingAddressFixture;
17+
use Magento\Framework\App\Area;
18+
use Magento\Framework\Exception\LocalizedException;
19+
use Magento\Framework\Exception\MailException;
20+
use Magento\Framework\Mail\EmailMessageInterface;
21+
use Magento\Quote\Test\Fixture\AddProductToCart as AddProductToCartFixture;
22+
use Magento\Quote\Test\Fixture\GuestCart as GuestCartFixture;
23+
use Magento\Sales\Api\Data\ShipmentInterface;
24+
use Magento\Sales\Api\InvoiceRepositoryInterface;
25+
use Magento\Sales\Api\OrderRepositoryInterface;
26+
use Magento\Sales\Model\Order;
27+
use Magento\Sales\Model\Service\InvoiceService;
28+
use Magento\Sales\Model\Order\ShipmentFactory;
29+
use Magento\Sales\Api\ShipmentRepositoryInterface;
30+
use Magento\Shipping\Model\ShipmentNotifier;
31+
use Magento\TestFramework\Fixture\Config;
32+
use Magento\TestFramework\Fixture\DataFixture;
33+
use Magento\TestFramework\Fixture\DataFixtureStorageManager;
34+
use Magento\TestFramework\Helper\Bootstrap;
35+
use Magento\TestFramework\Mail\Template\TransportBuilderMock;
36+
use PHPUnit\Framework\TestCase;
37+
38+
/**
39+
* Test shipment creation with offline payment method and async email notification.
40+
*
41+
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
42+
*/
43+
class AdminShipmentAsyncEmailTest extends TestCase
44+
{
45+
/**
46+
* @var TransportBuilderMock
47+
*/
48+
private TransportBuilderMock $transportBuilder;
49+
50+
/**
51+
* @var EmailMessageInterface[]
52+
*/
53+
private array $sentEmails = [];
54+
55+
/**
56+
* @return void
57+
*/
58+
protected function setUp(): void
59+
{
60+
parent::setUp();
61+
62+
$objectManager = Bootstrap::getObjectManager();
63+
$this->transportBuilder = $objectManager->get(TransportBuilderMock::class);
64+
$this->sentEmails = [];
65+
66+
$this->transportBuilder->setOnMessageSentCallback(
67+
function (EmailMessageInterface $message): void {
68+
$this->sentEmails[] = $message;
69+
}
70+
);
71+
}
72+
73+
/**
74+
* Tests shipment email async behavior: not sent immediately, then sent by cron.
75+
*
76+
* @return void
77+
* @throws LocalizedException
78+
* @throws MailException
79+
*/
80+
#[
81+
Config('payment/checkmo/active', '1'),
82+
Config('carriers/flatrate/active', '1'),
83+
Config('sales_email/general/async_sending', '1'),
84+
Config('sales_email/shipment/enabled', '1'),
85+
Config('sales_email/general/sending_limit', '10'),
86+
DataFixture(ProductFixture::class, as: 'product'),
87+
DataFixture(GuestCartFixture::class, as: 'cart'),
88+
DataFixture(AddProductToCartFixture::class, ['cart_id' => '$cart.id$', 'product_id' => '$product.id$']),
89+
DataFixture(SetBillingAddressFixture::class, ['cart_id' => '$cart.id$']),
90+
DataFixture(SetShippingAddressFixture::class, ['cart_id' => '$cart.id$']),
91+
DataFixture(SetGuestEmailFixture::class, ['cart_id' => '$cart.id$', 'email' => 'async-shipment@example.com']),
92+
DataFixture(SetDeliveryMethodFixture::class, ['cart_id' => '$cart.id$']),
93+
DataFixture(SetPaymentMethodFixture::class, ['cart_id' => '$cart.id$', 'method' => 'checkmo']),
94+
DataFixture(PlaceOrderFixture::class, ['cart_id' => '$cart.id$'], 'order'),
95+
]
96+
public function testShipmentAsyncEmailBehavior(): void
97+
{
98+
Bootstrap::getInstance()->loadArea(Area::AREA_ADMINHTML);
99+
$shipment = $this->createShipmentForOrder();
100+
$objectManager = Bootstrap::getObjectManager();
101+
$shipmentNotifier = $objectManager->get(ShipmentNotifier::class);
102+
$notifyResult = $shipmentNotifier->notify($shipment);
103+
$this->assertFalse(
104+
$notifyResult,
105+
'ShipmentNotifier::notify should defer sending when async mode is active.'
106+
);
107+
$this->assertCount(
108+
0,
109+
$this->sentEmails,
110+
'Email must not be sent immediately in async mode.'
111+
);
112+
$cron = $objectManager->get('SalesShipmentSendEmailsCron');
113+
$cron->execute();
114+
$this->assertCount(
115+
1,
116+
$this->sentEmails,
117+
'One shipment email should be dispatched after cron execution.'
118+
);
119+
$this->assertShipmentEmailContent($this->sentEmails[0]);
120+
}
121+
122+
/**
123+
* Creates an invoice for the given order.
124+
*
125+
* @param Order $order
126+
* @return void
127+
* @throws LocalizedException
128+
*/
129+
private function createInvoiceForOrder(Order $order): void
130+
{
131+
$objectManager = Bootstrap::getObjectManager();
132+
$invoiceService = $objectManager->get(InvoiceService::class);
133+
$invoiceRepository = $objectManager->get(InvoiceRepositoryInterface::class);
134+
$orderRepository = $objectManager->get(OrderRepositoryInterface::class);
135+
$invoice = $invoiceService->prepareInvoice($order);
136+
$invoice->register();
137+
$invoice->setSendEmail(false);
138+
$invoiceRepository->save($invoice);
139+
$orderRepository->save($order);
140+
}
141+
142+
/**
143+
* Gets order from fixture storage.
144+
*
145+
* @return Order
146+
*/
147+
private function getOrderFromFixture(): Order
148+
{
149+
$fixtures = DataFixtureStorageManager::getStorage();
150+
/** @var Order $fixtureOrder */
151+
$fixtureOrder = $fixtures->get('order');
152+
$objectManager = Bootstrap::getObjectManager();
153+
$orderRepository = $objectManager->get(OrderRepositoryInterface::class);
154+
return $orderRepository->get((int)$fixtureOrder->getEntityId());
155+
}
156+
157+
/**
158+
* Creates a shipment for the order from fixtures.
159+
*
160+
* @return ShipmentInterface
161+
* @throws LocalizedException
162+
*/
163+
private function createShipmentForOrder(): ShipmentInterface
164+
{
165+
$order = $this->getOrderFromFixture();
166+
$objectManager = Bootstrap::getObjectManager();
167+
$shipmentRepository = $objectManager->get(ShipmentRepositoryInterface::class);
168+
$shipmentFactory = $objectManager->get(ShipmentFactory::class);
169+
$orderRepository = $objectManager->get(OrderRepositoryInterface::class);
170+
$this->createInvoiceForOrder($order);
171+
$quantities = $this->calculateShippableQuantities($order);
172+
$shipment = $shipmentFactory->create($order, $quantities);
173+
$shipment->register();
174+
$shipment->setSendEmail(true);
175+
$shipmentRepository->save($shipment);
176+
$orderRepository->save($shipment->getOrder());
177+
178+
return $shipment;
179+
}
180+
181+
/**
182+
* Calculates shippable quantities for order items.
183+
*
184+
* @param Order $order
185+
* @return array
186+
*/
187+
private function calculateShippableQuantities(Order $order): array
188+
{
189+
$quantities = [];
190+
foreach ($order->getAllItems() as $orderItem) {
191+
if ($orderItem->getIsVirtual()) {
192+
continue;
193+
}
194+
$qtyToShip = $orderItem->getQtyOrdered() - $orderItem->getQtyShipped();
195+
if ($qtyToShip > 0) {
196+
$quantities[$orderItem->getItemId()] = $qtyToShip;
197+
}
198+
}
199+
return $quantities;
200+
}
201+
202+
/**
203+
* Asserts shipment email has correct content.
204+
*
205+
* @param EmailMessageInterface $email
206+
* @return void
207+
*/
208+
private function assertShipmentEmailContent(EmailMessageInterface $email): void
209+
{
210+
$this->assertInstanceOf(EmailMessageInterface::class, $email);
211+
$this->assertStringContainsString(
212+
'order has shipped',
213+
$email->getSubject(),
214+
'Email subject should contain shipment confirmation text.'
215+
);
216+
217+
// Assert getTo() returns a non-empty array
218+
$recipients = $email->getTo();
219+
$this->assertNotEmpty(
220+
$recipients,
221+
'Email should have at least one recipient.'
222+
);
223+
$this->assertIsArray(
224+
$recipients,
225+
'Email recipients should be returned as an array.'
226+
);
227+
228+
// Now safely access the first recipient
229+
$this->assertEquals(
230+
'async-shipment@example.com',
231+
$recipients[0]->getEmail(),
232+
'Email should be sent to the customer email address.'
233+
);
234+
}
235+
236+
/**
237+
* @return void
238+
*/
239+
protected function tearDown(): void
240+
{
241+
$this->transportBuilder->clean();
242+
$this->sentEmails = [];
243+
parent::tearDown();
244+
}
245+
}

0 commit comments

Comments
 (0)