33 * Copyright 2025 Adobe
44 * All Rights Reserved.
55 */
6-
76declare (strict_types=1 );
87
98namespace Magento \Checkout \Helper ;
2827 * @magentoAppIsolation enabled
2928 * @magentoDbIsolation enabled
3029 * @magentoDataFixture Magento/Checkout/_files/quote_with_virtual_product_and_address.php
30+ * @magentoConfigFixture default_store checkout/payment_failed/template payment_failed_template
31+ * @magentoConfigFixture default_store checkout/payment_failed/identity support
3132 *
3233 * @AllureSuite("Checkout")
3334 * @AllureFeature("Payment Failed Email")
@@ -40,52 +41,48 @@ class DataTest extends TestCase
4041 * @var ObjectManagerInterface
4142 */
4243 private ObjectManagerInterface $ objectManager ;
43-
4444 /**
4545 * Quote management service
4646 *
4747 * @var QuoteManagement
4848 */
4949 private QuoteManagement $ quoteManagement ;
50-
5150 /**
5251 * Quote factory
5352 *
5453 * @var QuoteFactory
5554 */
5655 private QuoteFactory $ quoteFactory ;
57-
5856 /**
5957 * Checkout data helper
6058 *
6159 * @var Data
6260 */
6361 private Data $ checkoutHelper ;
64-
6562 /**
6663 * Order repository
6764 *
6865 * @var OrderRepositoryInterface
6966 */
7067 private OrderRepositoryInterface $ orderRepository ;
71-
7268 /**
7369 * Transport builder mock
7470 *
7571 * @var TransportBuilderMock
7672 */
7773 private TransportBuilderMock $ transportBuilder ;
78-
7974 /**
8075 * Reserved order ID used in fixture.
8176 */
8277 private const FIXTURE_RESERVED_ORDER_ID = 'test_order_with_virtual_product ' ;
83-
8478 /**
8579 * Payment method code to use in test.
8680 */
8781 private const PAYMENT_METHOD = 'checkmo ' ;
88-
82+ /**
83+ * Payment failure message used in test.
84+ */
85+ private const PAYMENT_FAILURE_MESSAGE = 'Simulated payment failure ' ;
8986 /**
9087 * Set up required Magento services for the test.
9188 *
@@ -94,7 +91,6 @@ class DataTest extends TestCase
9491 protected function setUp (): void
9592 {
9693 parent ::setUp ();
97-
9894 $ this ->objectManager = Bootstrap::getObjectManager ();
9995 $ this ->quoteManagement = $ this ->objectManager ->get (QuoteManagement::class);
10096 $ this ->quoteFactory = $ this ->objectManager ->get (QuoteFactory::class);
@@ -107,48 +103,49 @@ protected function setUp(): void
107103 * Test sending the "payment failed" email for an order with a virtual product.
108104 *
109105 * This test verifies that:
110- * - The payment failure email is sent successfully.
106+ * - The payment failure email is sent successfully
111107 * - The email content does not include shipping address or shipping method
112- * since the product is virtual.
108+ * - The email contains appropriate payment failure information
109+ * - The email is properly formatted for virtual products
113110 *
114111 * @return void
115112 */
116113 public function testSendPaymentFailedEmail (): void
117114 {
118115 [$ order , $ quote ] = $ this ->prepareOrderFromFixtureQuote ();
119116 $ this ->simulatePaymentFailure ($ order );
120-
121117 $ this ->checkoutHelper ->sendPaymentFailedEmail (
122118 $ quote ,
123- (string )__ (' Simulated payment failure ' ),
119+ (string )__ (self :: PAYMENT_FAILURE_MESSAGE ),
124120 $ quote ->getPayment ()->getMethod (),
125121 $ quote ->getCheckoutMethod ()
126122 );
127-
128123 $ message = $ this ->transportBuilder ->getSentMessage ();
129124 $ this ->assertNotNull ($ message , 'Expected a payment failed email to be sent. ' );
130-
131- $ emailBody = $ message ->getBody ();
132- if (method_exists ($ emailBody , 'bodyToString ' )) {
133- $ emailContent = quoted_printable_decode ($ emailBody ->bodyToString ());
134- } elseif (method_exists ($ emailBody , 'getParts ' ) && isset ($ emailBody ->getParts ()[0 ])) {
135- $ emailContent = $ emailBody ->getParts ()[0 ]->getRawContent ();
136- } else {
137- $ this ->fail ('Unable to extract email content for assertion. ' );
138- }
139-
140- $ this ->assertStringNotContainsString (
141- 'Shipping Address ' ,
142- $ emailContent ,
143- 'Shipping address should not appear in the payment failed email for virtual product. '
144- );
145- $ this ->assertStringNotContainsString (
146- 'Shipping Method ' ,
147- $ emailContent ,
148- 'Shipping method should not appear in the payment failed email for virtual product. '
125+ $ emailContent = $ this ->extractEmailContent ($ message ->getBody ());
126+ $ this ->assertVirtualProductEmailContent ($ emailContent );
127+ }
128+ /**
129+ * Test payment failed email with custom checkout method.
130+ *
131+ * @return void
132+ */
133+ public function testSendPaymentFailedEmailWithCustomCheckoutMethod (): void
134+ {
135+ [$ order , $ quote ] = $ this ->prepareOrderFromFixtureQuote ();
136+ $ quote ->setCheckoutMethod ('custom_method ' );
137+ $ this ->simulatePaymentFailure ($ order );
138+ $ this ->checkoutHelper ->sendPaymentFailedEmail (
139+ $ quote ,
140+ (string )__ (self ::PAYMENT_FAILURE_MESSAGE ),
141+ $ quote ->getPayment ()->getMethod (),
142+ $ quote ->getCheckoutMethod ()
149143 );
144+ $ message = $ this ->transportBuilder ->getSentMessage ();
145+ $ this ->assertNotNull ($ message , 'Expected a payment failed email to be sent with custom checkout method. ' );
146+ $ emailContent = $ this ->extractEmailContent ($ message ->getBody ());
147+ $ this ->assertVirtualProductEmailContent ($ emailContent );
150148 }
151-
152149 /**
153150 * Prepare an order from a fixture quote containing a virtual product.
154151 *
@@ -162,20 +159,16 @@ private function prepareOrderFromFixtureQuote(): array
162159 /** @var Quote $quote */
163160 $ quote = $ this ->objectManager ->create (Quote::class)
164161 ->load (self ::FIXTURE_RESERVED_ORDER_ID , 'reserved_order_id ' );
165-
166162 $ this ->assertNotNull ($ quote ->getId (), 'Failed to load quote from fixture. ' );
167163 $ this ->assertNotEmpty ($ quote ->getAllItems (), 'Quote from fixture is empty. ' );
168-
164+ $ this -> assertTrue ( $ quote -> hasVirtualItems (), ' Quote should contain virtual items. ' );
169165 $ quote ->getPayment ()->setMethod (self ::PAYMENT_METHOD );
170-
166+ $ quote -> collectTotals ();
171167 $ order = $ this ->quoteManagement ->submit ($ quote );
172-
173168 $ this ->assertNotNull ($ order ->getId (), 'Order was not created from quote. ' );
174169 $ this ->assertNotEmpty ($ order ->getIncrementId (), 'Order increment ID is missing. ' );
175-
176170 return [$ order , $ quote ];
177171 }
178-
179172 /**
180173 * Simulate a payment failure by cancelling the order and adding a history comment.
181174 *
@@ -190,13 +183,145 @@ private function simulatePaymentFailure(Order $order): void
190183 $ order ->setState (Order::STATE_CANCELED )
191184 ->setStatus (Order::STATE_CANCELED )
192185 ->addCommentToStatusHistory ((string )__ ('Simulated: Payment failure due to gateway timeout. ' ));
193-
194186 $ this ->orderRepository ->save ($ order );
195-
196187 $ this ->assertSame (
197188 Order::STATE_CANCELED ,
198189 $ order ->getState (),
199190 'Order state should be canceled after simulating payment failure. '
200191 );
201192 }
193+ /**
194+ * Extract email content for testing from various email body formats.
195+ *
196+ * @param mixed $emailBody
197+ * @return string
198+ */
199+ private function extractEmailContent ($ emailBody ): string
200+ {
201+ // Try different methods to extract email content
202+ if (method_exists ($ emailBody , 'bodyToString ' )) {
203+ return quoted_printable_decode ($ emailBody ->bodyToString ());
204+ }
205+ if (method_exists ($ emailBody , 'getParts ' )) {
206+ $ parts = $ emailBody ->getParts ();
207+ if (!empty ($ parts ) && method_exists ($ parts [0 ], 'getRawContent ' )) {
208+ return $ parts [0 ]->getRawContent ();
209+ }
210+ }
211+ if (method_exists ($ emailBody , 'getContent ' )) {
212+ return $ emailBody ->getContent ();
213+ }
214+ if (method_exists ($ emailBody , '__toString ' )) {
215+ return (string )$ emailBody ;
216+ }
217+ $ this ->fail (
218+ 'Unable to extract email content. Email body type: ' . get_class ($ emailBody ) .
219+ '. Available methods: ' . implode (', ' , get_class_methods ($ emailBody ))
220+ );
221+ }
222+ /**
223+ * Assert virtual product email content meets requirements.
224+ *
225+ * @param string $emailContent
226+ * @return void
227+ */
228+ private function assertVirtualProductEmailContent (string $ emailContent ): void
229+ {
230+ // Negative assertions - what should NOT be included for virtual products
231+ $ this ->assertStringNotContainsString (
232+ 'Shipping Address ' ,
233+ $ emailContent ,
234+ 'Shipping address should not appear in payment failed email for virtual product. '
235+ );
236+ $ this ->assertStringNotContainsString (
237+ 'Shipping Method ' ,
238+ $ emailContent ,
239+ 'Shipping method should not appear in payment failed email for virtual product. '
240+ );
241+ $ this ->assertStringNotContainsString (
242+ 'Delivery ' ,
243+ $ emailContent ,
244+ 'Delivery information should not appear in payment failed email for virtual product. '
245+ );
246+ $ this ->assertStringNotContainsString (
247+ 'Ship to ' ,
248+ $ emailContent ,
249+ 'Ship to information should not appear in payment failed email for virtual product. '
250+ );
251+ // Positive assertions - what should be included
252+ $ this ->assertStringContainsString (
253+ self ::PAYMENT_FAILURE_MESSAGE ,
254+ $ emailContent ,
255+ 'Payment failure message should be present in email. '
256+ );
257+ $ this ->assertStringContainsString (
258+ self ::PAYMENT_METHOD ,
259+ $ emailContent ,
260+ 'Payment method should be mentioned in email. '
261+ );
262+ // Verify email is not empty
263+ $ this ->assertNotEmpty (
264+ trim ($ emailContent ),
265+ 'Email content should not be empty. '
266+ );
267+ // Verify email contains order information
268+ $ this ->assertThat (
269+ $ emailContent ,
270+ $ this ->logicalOr (
271+ $ this ->stringContains ('Order ' ),
272+ $ this ->stringContains ('Payment ' ),
273+ $ this ->stringContains ('Failed ' )
274+ ),
275+ 'Email should contain order or payment related information. '
276+ );
277+ }
278+ /**
279+ * Assert that email contains billing information but not shipping information.
280+ *
281+ * @param string $emailContent
282+ * @return void
283+ */
284+ private function assertBillingButNoShippingInformation (string $ emailContent ): void
285+ {
286+ // Billing information should be present
287+ $ this ->assertStringContainsString (
288+ 'Billing Address ' ,
289+ $ emailContent ,
290+ 'Billing address should be present in payment failed email. '
291+ );
292+ // Shipping information should not be present
293+ $ this ->assertStringNotContainsString (
294+ 'Shipping Address ' ,
295+ $ emailContent ,
296+ 'Shipping address should not be present for virtual products. '
297+ );
298+ }
299+ /**
300+ * Test that email template variables are properly set.
301+ *
302+ * @return void
303+ */
304+ public function testEmailTemplateVariables (): void
305+ {
306+ [$ order , $ quote ] = $ this ->prepareOrderFromFixtureQuote ();
307+ $ this ->simulatePaymentFailure ($ order );
308+ $ this ->checkoutHelper ->sendPaymentFailedEmail (
309+ $ quote ,
310+ (string )__ (self ::PAYMENT_FAILURE_MESSAGE ),
311+ $ quote ->getPayment ()->getMethod (),
312+ $ quote ->getCheckoutMethod ()
313+ );
314+ $ message = $ this ->transportBuilder ->getSentMessage ();
315+ $ this ->assertNotNull ($ message , 'Expected a payment failed email to be sent. ' );
316+ // Verify email headers
317+ $ headers = $ message ->getHeaders ();
318+ $ this ->assertNotNull ($ headers , 'Email headers should be present. ' );
319+ // Verify email subject
320+ $ subject = $ message ->getSubject ();
321+ $ this ->assertNotEmpty ($ subject , 'Email subject should not be empty. ' );
322+ // Verify email recipient
323+ $ to = $ message ->getTo ();
324+ $ this ->assertNotEmpty ($ to , 'Email recipient should be present. ' );
325+ }
202326}
327+
0 commit comments