Skip to content

Commit

Permalink
Append webhook controller.
Browse files Browse the repository at this point in the history
  • Loading branch information
arhitov committed Apr 23, 2024
1 parent a213ad9 commit 4bdd5fc
Show file tree
Hide file tree
Showing 9 changed files with 189 additions and 8 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ Billing module for Laravel projects with support for transactions, invoicing, su
- Saved credit cards
- Subscriptions
- Using omnipay gateway
- Webhook controller


### in developing
Expand Down
12 changes: 6 additions & 6 deletions composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions config/billing.php
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,14 @@
'secret' => 'test_Fh8hUAVVBGUGbjmlzba6TB0iyUbos_lueTHE-axOwM0',
],
'return_url' => 'https://www.example.com/pay',
'webhook' => [
'response' => [
'content' => null,
'status' => 200,
],
/** Trust incoming data. Otherwise, a request will be sent to the gateway API. */
'trust_input_data' => false,
],
],
'yookassa-two-step' => [
'omnipay_class' => 'YooKassa',
Expand Down
2 changes: 2 additions & 0 deletions src/Exceptions/Gateway/GatewayException.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Arhitov\LaravelBilling\Exceptions\Gateway;

use Arhitov\LaravelBilling\Exceptions\LaravelBillingException;
use Exception;

class GatewayException extends LaravelBillingException
{
Expand All @@ -15,6 +16,7 @@ class GatewayException extends LaravelBillingException
public function __construct(
public string $gateway,
string $msg = '',
public Exception|null $exception = null,
)
{
parent::__construct($msg);
Expand Down
11 changes: 11 additions & 0 deletions src/Exceptions/Gateway/GatewayNotSupportMethodException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

namespace Arhitov\LaravelBilling\Exceptions\Gateway;

class GatewayNotSupportMethodException extends GatewayException
{
public function __construct(string $gateway, public string $method, string $msg = '')
{
parent::__construct($gateway, $msg);
}
}
19 changes: 19 additions & 0 deletions src/Exceptions/Operation/OperationNotFoundException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

namespace Arhitov\LaravelBilling\Exceptions\Operation;

use Arhitov\LaravelBilling\Exceptions\OperationException;
use Arhitov\LaravelBilling\Models\Operation;

class OperationNotFoundException extends OperationException
{
/**
* Create a new exception instance.
*
* @param string|null $msg
*/
public function __construct(string $msg = null)
{
parent::__construct(new Operation, $msg ?? '');
}
}
51 changes: 51 additions & 0 deletions src/Http/Controllers/WebhookController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php

namespace Arhitov\LaravelBilling\Http\Controllers;

use Arhitov\LaravelBilling\Exceptions\Gateway\GatewayException;
use Arhitov\LaravelBilling\Exceptions\Operation\OperationNotFoundException;
use Arhitov\LaravelBilling\Models\Operation;
use Arhitov\LaravelBilling\OmnipayGateway;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Omnipay\Common\Exception\InvalidResponseException;

class WebhookController
{
public function webhookNotification(Request $request, string $gateway = null): Response
{
$omnipayGateway = new OmnipayGateway($gateway, httpRequest: $request);
$gateway = $omnipayGateway->getGatewayName();

/** @var \Omnipay\Common\Message\AbstractResponse $response */
$response = $omnipayGateway->notification()->send();

$gatewayPaymentId = $response->getTransactionReference();

if (! $omnipayGateway->getConfig('webhook.trust_input_data', false)) {
try {
/** @var \Omnipay\Common\Message\AbstractResponse $response */
$response = $omnipayGateway->details([
'transactionReference' => $gatewayPaymentId,
])->send();
} catch (InvalidResponseException $exception) {
throw new GatewayException($gateway, $exception->getMessage(), exception: $exception);
}
}

$gatewayPaymentId = $response->getTransactionReference();
/** @var Operation|null $operation */
$operation = Operation::query()
->where('gateway', '=', $gateway)
->where('gateway_payment_id', '=', $gatewayPaymentId)
->first();
if (! $operation) {
throw new OperationNotFoundException("Operation not found for {$gateway}:{$gatewayPaymentId}");
}

$operation->setStateByOmnipayGateway($response)
->saveOrFail();

return response($omnipayGateway->getConfig('webhook.response.content'), $omnipayGateway->getConfig('webhook.response.status', 201));
}
}
41 changes: 39 additions & 2 deletions src/OmnipayGateway.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,19 @@

use Arhitov\LaravelBilling\Exceptions\Gateway\GatewayNotFoundException;
use Arhitov\LaravelBilling\Exceptions\Gateway\GatewayNotSpecifiedException;
use Arhitov\LaravelBilling\Exceptions\Gateway\GatewayNotSupportMethodException;
use Omnipay\Common\GatewayInterface;
use Omnipay\Common\Message\AbstractRequest;
use Omnipay\Omnipay;
use Symfony\Component\HttpFoundation\Request;

class OmnipayGateway
{
private GatewayInterface $gateway;
private string $gatewayName;
private array $gatewayConfig;

public function __construct(string|null $gateway)
public function __construct(string|null $gateway, Request $httpRequest = null)
{

$gateway ??= config('billing.omnipay_gateway.default', null);
Expand All @@ -30,7 +33,7 @@ public function __construct(string|null $gateway)
$this->gatewayConfig = $gatewayConfig;

// Initialization gateway
$this->gateway = Omnipay::create($gatewayConfig['omnipay_class']);
$this->gateway = Omnipay::create($gatewayConfig['omnipay_class'], httpRequest: $httpRequest);
if (! empty($gatewayConfig['omnipay_initialize'])) {
$this->gateway->initialize($gatewayConfig['omnipay_initialize']);
}
Expand All @@ -52,6 +55,40 @@ public function getGatewayName(): string
return $this->gatewayName;
}

/**
* Get payment information.
*
* @param array $options
* @return AbstractRequest
* @throws GatewayNotSupportMethodException
*/
public function details(array $options = []): AbstractRequest
{
$gatewayInterface = $this->getGateway();
if (! method_exists($gatewayInterface, 'details')) {
throw new GatewayNotSupportMethodException($this->getGatewayName(), 'details');
}

return $gatewayInterface->details($options);
}

/**
* Input payment notification.
*
* @param array $options
* @return AbstractRequest
* @throws GatewayNotSupportMethodException
*/
public function notification(array $options = []): AbstractRequest
{
$gatewayInterface = $this->getGateway();
if (! method_exists($gatewayInterface, 'notification')) {
throw new GatewayNotSupportMethodException($this->getGatewayName(), 'notification');
}

return $gatewayInterface->notification($options);
}

/**
* @param string|null $key
* @param $default
Expand Down
52 changes: 52 additions & 0 deletions tests/Manual/Console/Commands/YooKassa/WebhookControllerTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?php

namespace Arhitov\LaravelBilling\Tests\Manual\Console\Commands\YooKassa;

use Arhitov\LaravelBilling\Http\Controllers\WebhookController;
use Arhitov\LaravelBilling\Models\Operation;
use Arhitov\LaravelBilling\OmnipayGateway;
use Arhitov\LaravelBilling\Tests\ConsoleCommandsTestCase;
use Illuminate\Http\Request;

class WebhookControllerTest extends ConsoleCommandsTestCase
{
const GATEWAY = 'yookassa';

public function testRequestByYookassa()
{
$owner = $this->createOwner();
$balance = $owner->getBalanceOrCreate();
$amount = '123.45';

$omnipayGateway = new OmnipayGateway(self::GATEWAY);

$this
->artisan('billing:create-payment', [
'balance' => $balance->getKey(),
'amount' => $amount,
'--gateway' => self::GATEWAY,
])
->assertSuccessful();

/** @var Operation|null $operation */
$operation = $balance->operation()->first();

$this->assertNotEmpty($operation->gateway_payment_id);
$this->assertEquals('pending', $operation->gateway_payment_state);

// Fixture incoming notification.payment.succeeded.json
$notification = json_decode(file_get_contents(__DIR__ . '/../../../../../vendor/arhitov/omnipay-yookassa/tests/Fixtures/fixture/notification.payment.succeeded.json'), true);

$notification['object']['id'] = $operation->gateway_payment_id;
$notification['object']['amount']['value'] = $amount;

$httpRequest = new Request(
content: json_encode($notification, JSON_UNESCAPED_UNICODE)
);

$response = (new WebhookController())->webhookNotification($httpRequest, 'yookassa');

$this->assertTrue($response->isSuccessful());
$this->assertEquals($omnipayGateway->getConfig('webhook.response.status', 201), $response->getStatusCode());
}
}

0 comments on commit 4bdd5fc

Please sign in to comment.