Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion tests/Integration/API/Mocks/MockedApi.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Mollie\WooCommerceTests\Integration\API\Mocks;

use Mollie\Api\Resources\Payment;
use Mollie\WooCommerce\SDK\Api;
use Mollie\Api\MollieApiClient;
use Mockery;
Expand Down Expand Up @@ -93,7 +94,7 @@ public function mockPaymentGet(string $paymentId, array $paymentData): self
*/
protected function createMockPaymentObject(array $paymentData)
{
$paymentObject = Mockery::mock('Payment');
$paymentObject = Mockery::mock(Payment::class);

foreach ($paymentData as $key => $value) {
$paymentObject->$key = $value;
Expand Down
369 changes: 369 additions & 0 deletions tests/Integration/spec/webhooks/WebhooksIntegrationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -178,4 +178,373 @@ public function it_handles_concurrent_webhook_calls_gracefully()
// Should only have one payment started note
$this->assertCount(1, $paymentNotes, 'Should only process payment once, even with concurrent webhooks');
}
/**
* Data provider for payment status webhook tests
*
* @return array
*/
public function paymentStatusProvider(): array
{
return [
'paid status' => [
'status' => 'paid',
'expectedOrderStatus' => 'processing',
'expectedMeta' => [],
'expectedNote' => 'Order completed using Mollie - iDEAL payment'
],
'authorized status' => [
'status' => 'authorized',
'expectedOrderStatus' => 'processing',//if the item is not virtual then it goes to processing
'expectedMeta' => ['_mollie_authorized' => '1'],
'expectedNote' => 'Order authorized using Mollie - iDEAL payment'
],
'failed status' => [
'status' => 'failed',
'expectedOrderStatus' => 'failed',
'expectedMeta' => [],
'expectedNote' => null
],
'canceled status' => [
'status' => 'canceled',
'expectedOrderStatus' => ['pending', 'cancelled'], // Can be either based on settings
'expectedMeta' => ['_mollie_cancelled_payment_id' => true], // true means check it exists
'expectedNote' => 'Mollie - iDEAL payment (tr_test_payment_canceled - test mode) cancelled'
],
'expired status' => [
'status' => 'expired',
'expectedOrderStatus' => ['pending', 'cancelled'],//ideal returns pending if expired in test mode
'expectedMeta' => [],
'expectedNote' => null
],
];
}

/**
* Test webhook processes different payment statuses correctly.
* See paymentStatusProvider for more details.
*
* @test
* @group integration
* @group Webhooks
* @dataProvider paymentStatusProvider
*/
public function it_processes_webhook_for_payment_status(
string $status,
$expectedOrderStatus,
array $expectedMeta,
?string $expectedNote
) {
$order = $this->getConfiguredOrder(
1,
'mollie_wc_gateway_ideal',
['simple'],
[],
false
);

$orderId = $order->get_id();
$orderKey = $order->get_order_key();
$paymentId = 'tr_test_payment_' . $status;

$this->mockSuccessfulPaymentGet($paymentId, $status, [
'metadata' => ['order_id' => $orderId],
'method' => 'ideal',
'mode' => 'test'
]);

$mockedServices = $this->getMockedApiServices();
$container = $this->bootstrapModule($mockedServices);

$this->setupWebhookRequest($orderId, $orderKey, $paymentId);

$webhookService = $this->createMockedWebhookService($container, $paymentId);
$webhookService->onWebhookAction();

$order = wc_get_order($orderId);

// Check order status
if (is_array($expectedOrderStatus)) {
$this->assertContains($order->get_status(), $expectedOrderStatus);
} else {
$this->assertEquals($expectedOrderStatus, $order->get_status());
}

// Check expected meta
foreach ($expectedMeta as $metaKey => $metaValue) {
if ($metaValue === true) {
$this->assertNotEmpty($order->get_meta($metaKey));
} else {
$this->assertEquals($metaValue, $order->get_meta($metaKey));
}
}

// Check order notes if expected
if ($expectedNote !== null) {
$notes = wc_get_order_notes(['order_id' => $orderId]);
var_dump($notes);
var_dump($expectedNote);
$hasExpectedNote = false;
foreach ($notes as $note) {
if (strpos($note->content, $expectedNote) !== false) {
$hasExpectedNote = true;
break;
}
}
$this->assertTrue($hasExpectedNote, "Expected note containing '{$expectedNote}' not found");
}
}


/**
* Test webhook processes full refund correctly
* GIVEN that a payment has been made and the order is marked as paid
* WHEN the refund webhook is triggered
* THEN the order status is updated to 'refunded'
* AND the order note is updated to 'New refund'
* AND the order total is updated correctly
*
* @test
* @group integration
* @group Webhooks
*/
public function it_processes_refund_webhook_correctly()
{
$order = $this->getConfiguredOrder(
1,
'mollie_wc_gateway_ideal',
['simple'],
[],
false
);

$orderId = $order->get_id();
$orderKey = $order->get_order_key();
$paymentId = 'tr_refund_test_payment';
$refundId = 're_test_refund';

// First mark order as paid
$order->payment_complete($paymentId);
// Set the meta to make isOrderPaidAndProcessed return true
$order->update_meta_data('_mollie_paid_and_processed', '1');
$order->save();

// Mock payment with refunds
$paymentData = [
'id' => $paymentId,
'status' => 'paid',
'amount' => (object)[
'value' => '11.00',//10+tax
'currency' => 'EUR'
],
'amountRefunded' => (object)[
'value' => '11.00', // Full refund to trigger status change
'currency' => 'EUR'
],
'metadata' => (object)['order_id' => $orderId],
'method' => 'ideal',
'mode' => 'test',
/*'_links' => (object)[
'refunds' => [
'href' => 'https://api.mollie.com/v2/payments/'.$paymentId.'/refunds',
'type' => 'application/hal+json'
]
],*/ //this would be present in the API response, but would make us call the API in the test, so we use _embedded
'_embedded' => (object)[
'refunds' => [
(object)[
'id' => $refundId,
'amount' => [
'value' => '11.00',
'currency' => 'EUR'
],
'status' => 'refunded',
'createdAt' => '2023-01-01T12:00:00+00:00'
]
]
]
];

$this->apiMock()->mockPaymentGet($paymentId, $paymentData);

// Mock refunds endpoint
$this->apiMock()->getMockedApiClient()->refunds
->shouldReceive('listForPayment')
->with($paymentId)
->andReturn((object)[
'count' => 1,
'_embedded' => [
'refunds' => [
(object)[
'id' => $refundId,
'amount' => [
'value' => '11.00',
'currency' => 'EUR'
],
'status' => 'refunded',
'createdAt' => '2023-01-01T12:00:00+00:00'
]
]
]
]);

$mockedServices = $this->getMockedApiServices();
$container = $this->bootstrapModule($mockedServices);

$this->setupWebhookRequest($orderId, $orderKey, $paymentId);

// Create webhook service but don't mock processRefunds - let it run
$webhookService = $this->createMockedWebhookService($container, $paymentId);

// Allow the actual methods to be called
$webhookService->shouldReceive('notifyProcessedRefunds')
->passthru();

$webhookService->shouldReceive('processUpdateStateRefund')
->passthru();

// Track if the action was fired
$actionFired = false;
add_action($container->get('shared.plugin_id') . '_refunds_processed', function() use (&$actionFired) {
$actionFired = true;
});

// Execute webhook
$webhookService->onWebhookAction();

// Verify the refund was processed
$order = wc_get_order($orderId);

// Check order status was changed to refunded (full refund)
$this->assertEquals('refunded', $order->get_status(), 'Order should be marked as refunded');

// Check refund note was added
$notes = wc_get_order_notes(['order_id' => $orderId]);
$refundNotes = array_filter($notes, function ($note) use ($refundId) {
return strpos($note->content, 'New refund') !== false &&
strpos($note->content, $refundId) !== false;
});
$this->assertNotEmpty($refundNotes, 'Refund note should be added');

// Check processed refund IDs were saved
$processedRefundIds = $order->get_meta('_mollie_processed_refund_ids');
$this->assertContains($refundId, $processedRefundIds, 'Refund ID should be marked as processed');

// Verify action was fired
$this->assertTrue($actionFired, 'Refunds processed action should be fired');
}

/**
* Test webhook handles partial refund correctly
* GIVEN that a payment has been made and the order is marked as paid
* WHEN the refund webhook is triggered
* AND the refund is less than the full amount
* THEN the refund should be processed correctly
* BUT order total and order status is NOT updated
* AND the order note is updated to 'Partial refund'
*
* @test
* @group integration
* @group Webhooks
*/
public function it_processes_partial_refund_webhook_correctly()
{
$order = $this->getConfiguredOrder(
1,
'mollie_wc_gateway_ideal',
['simple'],
[],
false
);

$orderId = $order->get_id();
$orderKey = $order->get_order_key();
$paymentId = 'tr_partial_refund_payment';
$refundId = 're_partial_refund';

// Mark order as paid
$order->payment_complete($paymentId);
$order->update_meta_data('_mollie_paid_and_processed', '1');
$order->save();

// Mock payment with partial refund
$paymentData = [
'id' => $paymentId,
'status' => 'paid',
'amount' => (object)[
'value' => '11.00',
'currency' => 'EUR'
],
'amountRefunded' => (object)[
'value' => '3.00', // Partial refund
'currency' => 'EUR'
],
'metadata' => (object)['order_id' => $orderId],
'method' => 'ideal',
'mode' => 'test',
/*'_links' => (object)[
'refunds' => (object)[
'href' => 'https://api.mollie.com/v2/payments/'.$paymentId.'/refunds',
'type' => 'application/hal+json'
]
],*/ //this would be present in the API response, but would make us call the API in the test, so we use _embedded
'_embedded' => (object)[
'refunds' => [
(object)[
'id' => $refundId,
'amount' => [
'value' => '3.00',
'currency' => 'EUR'
],
'status' => 'refunded',
'createdAt' => '2023-01-01T12:00:00+00:00'
]
]
]
];

$this->apiMock()->mockPaymentGet($paymentId, $paymentData);

// Mock refunds endpoint
$this->apiMock()->getMockedApiClient()->refunds
->shouldReceive('listForPayment')
->with($paymentId)
->andReturn((object)[
'count' => 1,
'_embedded' => (object)[
'refunds' => [
(object)[
'id' => $refundId,
'amount' => [
'value' => '3.00',
'currency' => 'EUR'
],
'status' => 'refunded',
'createdAt' => '2023-01-01T12:00:00+00:00'
]
]
]
]);

$mockedServices = $this->getMockedApiServices();
$container = $this->bootstrapModule($mockedServices);

$this->setupWebhookRequest($orderId, $orderKey, $paymentId);

$webhookService = $this->createMockedWebhookService($container, $paymentId);

// Mock internal methods
$webhookService->shouldReceive('notifyProcessedRefunds')->passthru();
$webhookService->shouldReceive('processUpdateStateRefund')->passthru();

$webhookService->onWebhookAction();

$order = wc_get_order($orderId);

// For partial refund, status should NOT change to refunded
$this->assertNotEquals('refunded', $order->get_status(), 'Order should not be fully refunded');

// But refund should still be processed
$processedRefundIds = $order->get_meta('_mollie_processed_refund_ids');
$this->assertContains($refundId, $processedRefundIds, 'Partial refund should be marked as processed');
}
}