'WH-EVENT-1', 'event_type' => 'PAYMENT.CAPTURE.COMPLETED', 'resource' => [ 'id' => 'CAPTURE-1', 'status' => 'COMPLETED', 'amount' => [ 'value' => '100.00', 'currency_code' => 'USD', ], 'supplementary_data' => [ 'related_ids' => [ 'order_id' => 'PAYPAL-ORDER-1', ], ], ], ], $overrides); } public function test_it_resolves_and_settles_a_completed_capture(): void { $attempt = new PaymentAttempt([ 'order_id' => 10, 'payment_method' => 'paypal_smart_button', ]); $order = (object) [ 'id' => 10, 'payment' => (object) [ 'method' => 'paypal_smart_button', 'additional' => ['paypal_order_id' => 'PAYPAL-ORDER-1'], ], ]; $attempts = Mockery::mock(PaymentAttemptRepository::class); $attempts->shouldReceive('findByGatewayOrderId') ->once() ->with('PAYPAL-ORDER-1') ->andReturn($attempt); $orders = Mockery::mock(OrderRepository::class); $orders->shouldReceive('find')->once()->with(10)->andReturn($order); $paymentService = Mockery::mock(PaymentService::class); $paymentService->shouldReceive('completeWebhookCapture') ->once() ->withArgs(function ($actualOrder, $gatewayOrderId, array $capture, $eventId) use ($order) { return $actualOrder === $order && $gatewayOrderId === 'PAYPAL-ORDER-1' && $capture['transaction_id'] === 'CAPTURE-1' && $capture['capture_status'] === 'COMPLETED' && $capture['amount'] === 100.0 && $capture['currency'] === 'USD' && $eventId === 'WH-EVENT-1'; }) ->andReturn(['gatewayStatus' => 'captured', 'order' => $order]); $service = new WebhookService($attempts, $orders, $paymentService); $result = $service->handleCaptureCompleted($this->event()); $this->assertSame('captured', $result['gatewayStatus']); } public function test_it_rejects_an_event_without_a_related_paypal_order(): void { $service = new WebhookService( Mockery::mock(PaymentAttemptRepository::class), Mockery::mock(OrderRepository::class), Mockery::mock(PaymentService::class), ); $event = $this->event(); unset($event['resource']['supplementary_data']); $this->expectException(\InvalidArgumentException::class); $this->expectExceptionMessage('PayPal order id is missing'); $service->handleCaptureCompleted($event); } public function test_it_rejects_a_gateway_order_mismatch(): void { $attempt = new PaymentAttempt([ 'order_id' => 10, 'payment_method' => 'paypal_smart_button', ]); $order = (object) [ 'id' => 10, 'payment' => (object) [ 'method' => 'paypal_smart_button', 'additional' => ['paypal_order_id' => 'ANOTHER-ORDER'], ], ]; $attempts = Mockery::mock(PaymentAttemptRepository::class); $attempts->shouldReceive('findByGatewayOrderId')->andReturn($attempt); $orders = Mockery::mock(OrderRepository::class); $orders->shouldReceive('find')->andReturn($order); $service = new WebhookService( $attempts, $orders, Mockery::mock(PaymentService::class), ); $this->expectException(\InvalidArgumentException::class); $this->expectExceptionMessage('does not match'); $service->handleCaptureCompleted($this->event()); } public function test_it_rejects_a_non_completed_capture(): void { $attempt = new PaymentAttempt([ 'order_id' => 10, 'payment_method' => 'paypal_smart_button', ]); $order = (object) [ 'id' => 10, 'payment' => (object) [ 'method' => 'paypal_smart_button', 'additional' => ['paypal_order_id' => 'PAYPAL-ORDER-1'], ], ]; $attempts = Mockery::mock(PaymentAttemptRepository::class); $attempts->shouldReceive('findByGatewayOrderId')->andReturn($attempt); $orders = Mockery::mock(OrderRepository::class); $orders->shouldReceive('find')->andReturn($order); $service = new WebhookService( $attempts, $orders, Mockery::mock(PaymentService::class), ); $this->expectException(\InvalidArgumentException::class); $this->expectExceptionMessage('not completed'); $service->handleCaptureCompleted($this->event([ 'resource' => ['status' => 'PENDING'], ])); } }