10 Commits
3.2.0 ... 3.2.1

Author SHA1 Message Date
f17519743e update jwt encode/decode
Signed-off-by: envoyr <hello@envoyr.com>
2023-02-20 16:22:06 +01:00
aea65e0894 update firebase and illuminate
Signed-off-by: envoyr <hello@envoyr.com>
2023-02-20 14:53:12 +01:00
ce36527fe0 Update Subscriptions.php 2022-10-03 11:15:54 +02:00
c0dee13277 Update UsersTrait.php 2022-10-02 23:25:42 +02:00
4fae63297c Update Provider.php 2022-10-02 22:15:26 +02:00
f8439f81e9 Update Provider.php 2022-10-02 22:12:46 +02:00
95b8559b01 Update Scope.php 2022-10-02 12:51:51 +02:00
441900e1d3 Update Orders.php 2022-10-01 14:42:01 +02:00
René Preuß
f5e9b0e5e6 Refactoring 2022-10-01 14:03:00 +02:00
René Preuß
a2405875ad Refactoring 2022-10-01 14:00:25 +02:00
34 changed files with 903 additions and 514 deletions

View File

@@ -15,11 +15,11 @@
"require": { "require": {
"php": "^7.4|^8.0", "php": "^7.4|^8.0",
"ext-json": "*", "ext-json": "*",
"illuminate/support": "~5.4|~5.7.0|~5.8.0|^6.0|^7.0|^8.0|^9.0", "illuminate/support": "~5.4|~5.7.0|~5.8.0|^6.0|^7.0|^8.0|^9.0|^10.0",
"illuminate/console": "~5.4|~5.7.0|~5.8.0|^6.0|^7.0|^8.0|^9.0", "illuminate/console": "~5.4|~5.7.0|~5.8.0|^6.0|^7.0|^8.0|^9.0|^10.0",
"guzzlehttp/guzzle": "^6.3|^7.0", "guzzlehttp/guzzle": "^6.3|^7.0",
"socialiteproviders/manager": "^3.4|^4.0.1", "socialiteproviders/manager": "^3.4|^4.0.1",
"firebase/php-jwt": "^5.2" "firebase/php-jwt": "^6.0"
}, },
"require-dev": { "require-dev": {
"phpunit/phpunit": "^8.0|^9.0", "phpunit/phpunit": "^8.0|^9.0",
@@ -28,12 +28,15 @@
}, },
"autoload": { "autoload": {
"psr-4": { "psr-4": {
"Bitinflow\\Accounts\\": "src/Accounts" "Bitinflow\\Accounts\\": "src/Accounts",
"Bitinflow\\Payments\\": "src/Payments",
"Bitinflow\\Support\\": "src/Support"
} }
}, },
"autoload-dev": { "autoload-dev": {
"psr-4": { "psr-4": {
"Bitinflow\\Accounts\\Tests\\": "tests/Accounts" "Bitinflow\\Accounts\\Tests\\": "tests/Accounts",
"Bitinflow\\Payments\\Tests\\": "tests/Payments"
} }
}, },
"scripts": { "scripts": {

View File

@@ -20,4 +20,11 @@
<directory suffix=".php">src</directory> <directory suffix=".php">src</directory>
</whitelist> </whitelist>
</filter> </filter>
<php>
<env name="BITINFLOW_ACCOUNTS_KEY" value="38"/>
<env name="BITINFLOW_ACCOUNTS_SECRET" value="2goNRF8x37HPVZVaa28ySZGVXJuksvxnxM7TcGzM"/>
<env name="BITINFLOW_PAYMENTS_BASE_URL" value="https://api.sandbox.pay.bitinflow.com/v1/"/>
<env name="BITINFLOW_PAYMENTS_DASHBOARD_URL" value="https://sandbox.pay.bitinflow.com/v1/"/>
<env name="CLIENT_ACCESS_TOKEN" value="eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJhdWQiOiIxNSIsImp0aSI6IjAxN2QxZDg0Y2MxNjAyZTYxOGZkNjYwZGViZWVjMDY4MTk2YmYzMDk1OGMzY2RiYzBjZmJkZWFjZjFhOTUxODQzZDU1YTk3OGY2YWIxY2YzIiwiaWF0IjoxNjY0NjIzMzA4LjQyODI4MSwibmJmIjoxNjY0NjIzMzA4LjQyODI4NCwiZXhwIjoxNjgwMzQ4MTA4LjQxNjc4MSwic3ViIjoiMzkiLCJzY29wZXMiOlsiYXBpIiwicmVhZF91c2VyIl0sImNsaWVudCI6eyJ0cnVzdGVkIjpmYWxzZX19.vxnzCaU4PpOrNVHa5AnGSS6gX_RCvxIERAnHFhjTrUzRafV9mr2Cvwd-BDVYoUr10wHvxa_TJSYfnAdDuhE-MEyDv13O3dL2XGTtJNa_Rg6L6CQ0JvC3wL-lWPvGPFax9pu-_lqbA3jm5B08hc3-7tq3f2nXcxjhtkqT6TTJv1-RCAppb2HCXiUDAqANzbhyInDjOH2WvFj1OGC_AI03J3W2KRWyeFLtUne8XKPFyr9XGcPuTrqogcuuXLeUt2kcf2bXbuIV1OlgIECrDiP1Ee0F2AzPs27ZVJ2z0R0JbT6AubKhGl5_Qi27cwjOH7hT2dmjcF1mLjzpN1uChLIdSnGSoStH8VzYHnHE2I8G-owW_aadG2UmGdnRY143q6g_28f3WIZNSucBSXkwFeS_t4fylsvpxhpjYJusf5qiEU_X3YbeawYMUCFUkSD2XTIypAqMJLNZQAeJ52eyL-9fln-Bv7n9v7K9G6ieR6Tm0tsJ1PRnaQi7rA1NTFwHoQmIOW9tfMycLzT7bgSoz3ra6Ez2J7ZNuWBZNKS0O-0YfSrAWyWK5U8YRfQuSVzP2VrIU63K6RGU2c284PfQGy11kgKUNQPykirb8p7MDQ8PwrxKaylBnD6hhDgjqEh2bfsr_43DfJA0R58L1HK3BmQnxgap0C77wK1e0yNlABpN28Q"/>
</php>
</phpunit> </phpunit>

View File

@@ -0,0 +1,19 @@
<?php
namespace Bitinflow\Accounts\ApiOperations;
use Bitinflow\Accounts\Exceptions\RequestRequiresMissingParametersException;
use Illuminate\Support\Arr;
trait Validation
{
/**
* @throws RequestRequiresMissingParametersException
*/
public function validateRequired(array $parameters, array $required)
{
if (!Arr::has($parameters, $required)) {
throw RequestRequiresMissingParametersException::fromValidateRequired($parameters, $required);
}
}
}

View File

@@ -77,6 +77,6 @@ class ApiTokenCookieFactory
'sub' => $userId, 'sub' => $userId,
'csrf' => $csrfToken, 'csrf' => $csrfToken,
'expiry' => $expiration->getTimestamp(), 'expiry' => $expiration->getTimestamp(),
], $this->encrypter->getKey()); ], $this->encrypter->getKey(), 'RS256');
} }
} }

View File

@@ -7,6 +7,7 @@ use Bitinflow\Accounts\Helpers\JwtParser;
use Bitinflow\Accounts\Traits\HasBitinflowTokens; use Bitinflow\Accounts\Traits\HasBitinflowTokens;
use Exception; use Exception;
use Firebase\JWT\JWT; use Firebase\JWT\JWT;
use Firebase\JWT\Key;
use Illuminate\Auth\AuthenticationException; use Illuminate\Auth\AuthenticationException;
use Illuminate\Auth\GuardHelpers; use Illuminate\Auth\GuardHelpers;
use Illuminate\Container\Container; use Illuminate\Container\Container;
@@ -181,8 +182,10 @@ class TokenGuard
{ {
return (array)JWT::decode( return (array)JWT::decode(
CookieValuePrefix::remove($this->encrypter->decrypt($request->cookie(BitinflowAccounts::cookie()), BitinflowAccounts::$unserializesCookies)), CookieValuePrefix::remove($this->encrypter->decrypt($request->cookie(BitinflowAccounts::cookie()), BitinflowAccounts::$unserializesCookies)),
$this->encrypter->getKey(), new Key(
['HS256'] $this->encrypter->getKey(),
'RS256'
)
); );
} }

View File

@@ -8,6 +8,7 @@ use Bitinflow\Accounts\Exceptions\RequestRequiresClientIdException;
use Bitinflow\Accounts\Exceptions\RequestRequiresRedirectUriException; use Bitinflow\Accounts\Exceptions\RequestRequiresRedirectUriException;
use Bitinflow\Accounts\Helpers\Paginator; use Bitinflow\Accounts\Helpers\Paginator;
use Bitinflow\Accounts\Traits; use Bitinflow\Accounts\Traits;
use Bitinflow\Support\Query;
use GuzzleHttp\Client; use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException; use GuzzleHttp\Exception\GuzzleException;
use GuzzleHttp\Exception\RequestException; use GuzzleHttp\Exception\RequestException;
@@ -18,13 +19,10 @@ use Illuminate\Contracts\Auth\Authenticatable;
*/ */
class BitinflowAccounts class BitinflowAccounts
{ {
use Traits\OauthTrait; use Traits\OauthTrait;
use Traits\SshKeysTrait; use Traits\SshKeysTrait;
use Traits\UsersTrait; use Traits\UsersTrait;
use Traits\HasBitinflowPaymentsWallet;
use ApiOperations\Delete; use ApiOperations\Delete;
use ApiOperations\Get; use ApiOperations\Get;
use ApiOperations\Post; use ApiOperations\Post;
@@ -35,61 +33,51 @@ class BitinflowAccounts
* *
* @var string * @var string
*/ */
public static $cookie = 'bitinflow_token'; public static string $cookie = 'bitinflow_token';
/** /**
* Indicates if Bitinflow Accounts should ignore incoming CSRF tokens. * Indicates if Bitinflow Accounts should ignore incoming CSRF tokens.
*
* @var bool
*/ */
public static $ignoreCsrfToken = false; public static bool $ignoreCsrfToken = false;
/** /**
* Indicates if Bitinflow Accounts should unserializes cookies. * Indicates if Bitinflow Accounts should unserializes cookies.
*
* @var bool
*/ */
public static $unserializesCookies = false; public static bool $unserializesCookies = false;
private static $baseUrl = 'https://accounts.bitinflow.com/api/';
private static string $baseUrl = 'https://accounts.bitinflow.com/api/';
/** /**
* Guzzle is used to make http requests. * Guzzle is used to make http requests.
*
* @var Client
*/ */
protected $client; protected Client $client;
/** /**
* Paginator object. * Paginator object.
*
* @var Paginator
*/ */
protected $paginator; protected Paginator $paginator;
/** /**
* bitinflow Accounts OAuth token. * bitinflow Accounts OAuth token.
* *
* @var string|null
*/ */
protected $token = null; protected ?string $token = null;
/** /**
* bitinflow Accounts client id. * bitinflow Accounts client id.
* *
* @var string|null
*/ */
protected $clientId = null; protected ?string $clientId = null;
/** /**
* bitinflow Accounts client secret. * bitinflow Accounts client secret.
*
* @var string|null
*/ */
protected $clientSecret = null; protected ?string $clientSecret = null;
/** /**
* bitinflow Accounts OAuth redirect url. * bitinflow Accounts OAuth redirect url.
*
* @var string|null
*/ */
protected $redirectUri = null; protected ?string $redirectUri = null;
/** /**
* Constructor. * Constructor.
@@ -129,7 +117,7 @@ class BitinflowAccounts
* @param string|null $cookie * @param string|null $cookie
* @return string|static * @return string|static
*/ */
public static function cookie($cookie = null) public static function cookie(string $cookie = null)
{ {
if (is_null($cookie)) { if (is_null($cookie)) {
return static::$cookie; return static::$cookie;
@@ -268,7 +256,7 @@ class BitinflowAccounts
* @return string|null * @return string|null
* @throws RequestRequiresAuthenticationException * @throws RequestRequiresAuthenticationException
*/ */
public function getToken() public function getToken(): ?string
{ {
if (!$this->token) { if (!$this->token) {
throw new RequestRequiresAuthenticationException; throw new RequestRequiresAuthenticationException;
@@ -323,7 +311,7 @@ class BitinflowAccounts
* @param string $method HTTP method * @param string $method HTTP method
* @param string $path Query path * @param string $path Query path
* @param array $parameters Query parameters * @param array $parameters Query parameters
* @param Paginator $paginator Paginator object * @param Paginator|null $paginator Paginator object
* @param mixed|null $jsonBody JSON data * @param mixed|null $jsonBody JSON data
* *
* @return Result Result object * @return Result Result object
@@ -332,13 +320,14 @@ class BitinflowAccounts
*/ */
public function query(string $method = 'GET', string $path = '', array $parameters = [], Paginator $paginator = null, $jsonBody = null): Result public function query(string $method = 'GET', string $path = '', array $parameters = [], Paginator $paginator = null, $jsonBody = null): Result
{ {
/** @noinspection DuplicatedCode */
if ($paginator !== null) { if ($paginator !== null) {
$parameters[$paginator->action] = $paginator->cursor(); $parameters[$paginator->action] = $paginator->cursor();
} }
try { try {
$response = $this->client->request($method, $path, [ $response = $this->client->request($method, $path, [
'headers' => $this->buildHeaders($jsonBody ? true : false), 'headers' => $this->buildHeaders((bool)$jsonBody),
'query' => $this->buildQuery($parameters), 'query' => Query::build($parameters),
'json' => $jsonBody ?: null, 'json' => $jsonBody ?: null,
]); ]);
$result = new Result($response, null, $paginator); $result = new Result($response, null, $paginator);
@@ -401,26 +390,6 @@ class BitinflowAccounts
$this->clientId = $clientId; $this->clientId = $clientId;
} }
/**
* Build query with support for multiple smae first-dimension keys.
*
* @param array $query
*
* @return string
*/
public function buildQuery(array $query): string
{
$parts = [];
foreach ($query as $name => $value) {
$value = (array)$value;
array_walk_recursive($value, function ($value) use (&$parts, $name) {
$parts[] = urlencode($name) . '=' . urlencode($value);
});
}
return implode('&', $parts);
}
/** /**
* @param string $path * @param string $path
* @param array $parameters * @param array $parameters

View File

@@ -11,14 +11,52 @@ class Scope
{ {
/* /*
* v0 API * v3 API
*/ */
// Deprecated scope. public const USER = 'user';
public const API = 'api'; public const USER_READ = 'user:read';
// Read nonpublic user information, including email address. public const USERS = 'users';
public const READ_USER = 'read_user'; public const USERS_READ = 'users:read';
public const USERS_CREATE = 'users:create';
public const PAYMENTS = 'payments';
public const PAYMENTS_READ = 'payments:read';
public const PAYMENTS_CREATE = 'payments:create';
public const PAYMENTS_CHECKOUT = 'payments:checkout';
public const PAYMENTS_REVOKE = 'payments:revoke';
public const PAYMENTS_RESUME = 'payments:resume';
public const PAYMENT_ORDERS = 'payment.orders';
public const PAYMENT_ORDERS_READ = 'payment.orders:read';
public const PAYMENT_ORDERS_CREATE = 'payment.orders:create';
public const PAYMENT_ORDERS_CHECKOUT = 'payment.orders:checkout';
public const PAYMENT_ORDERS_REVOKE = 'payment.orders:revoke';
public const PAYMENT_INVOICES = 'payment.invoices';
public const PAYMENT_INVOICES_READ = 'payment.invoices:read';
public const PAYMENT_SUBSCRIPTIONS = 'payment.subscriptions';
public const PAYMENT_SUBSCRIPTIONS_READ = 'payment.subscriptions:read';
public const PAYMENT_SUBSCRIPTIONS_CREATE = 'payment.subscriptions:create';
public const PAYMENT_SUBSCRIPTIONS_CHECKOUT = 'payment.subscriptions:checkout';
public const PAYMENT_SUBSCRIPTIONS_REVOKE = 'payment.subscriptions:revoke';
public const PAYMENT_SUBSCRIPTIONS_RESUME = 'payment.subscriptions:resume';
public const PAYMENT_WALLETS = 'payment.wallets';
public const PAYMENT_WALLETS_READ = 'payment.wallets:read';
public const PAYMENT_WALLETS_CREATE = 'payment.wallets:create';
public const PAYMENT_CHECKOUT_SESSIONS = 'payment.checkout-sessions';
public const PAYMENT_CHECKOUT_SESSIONS_READ = 'payment.checkout-sessions:read';
public const PAYMENT_CHECKOUT_SESSIONS_CREATE = 'payment.checkout-sessions:create';
public const PAYMENT_CHECKOUT_SESSIONS_CHECKOUT = 'payment.checkout-sessions:checkout';
public const PAYMENT_CHECKOUT_SESSIONS_REVOKE = 'payment.checkout-sessions:revoke';
/**
* v2 API
*/
/* /*
* v1 API * v1 API
@@ -30,11 +68,22 @@ class Scope
// Manage a authorized user object. // Manage a authorized user object.
public const USERS_EDIT = 'users:edit'; public const USERS_EDIT = 'users:edit';
public const USERS_CREATE = 'users:create'; // also available in v3
// public const USERS_CREATE = 'users:create';
// Read authorized user´s transactions. // Read authorized user´s transactions.
public const TRANSACTIONS_READ = 'transactions:read'; public const TRANSACTIONS_READ = 'transactions:read';
// Create a new charge for the authorized user. // Create a new charge for the authorized user.
public const CHARGES_CREATE = 'charges:create'; public const CHARGES_CREATE = 'charges:create';
}
/*
* v0 API
*/
// Deprecated scope.
public const API = 'api';
// Read nonpublic user information, including email address.
public const READ_USER = 'read_user';
}

View File

@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace Bitinflow\Accounts\Exceptions;
use Exception;
/**
* @author René Preuß <rene@preuss.io>
*/
class RequestRequiresMissingParametersException extends Exception
{
public function __construct($message = 'Request requires missing parameters', $code = 0, Exception $previous = null)
{
parent::__construct($message, $code, $previous);
}
public static function fromValidateRequired(array $given, array $required): RequestRequiresMissingParametersException
{
return new self(sprintf(
'Request requires missing parameters. Required: %s. Given: %s',
implode(', ', $required),
implode(', ', $given)
));
}
}

View File

@@ -5,14 +5,17 @@ declare(strict_types=1);
namespace Bitinflow\Accounts\Facades; namespace Bitinflow\Accounts\Facades;
use Bitinflow\Accounts\BitinflowAccounts as BitinflowAccountsService; use Bitinflow\Accounts\BitinflowAccounts as BitinflowAccountsService;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Support\Facades\Facade; use Illuminate\Support\Facades\Facade;
/** /**
* @author René Preuß <rene@preuss.io> * @method static string|static cookie(string $cookie = null)
* @method static Authenticatable actingAs($user, $scopes = [], $guard = 'api')
* @method static static withClientId(string $clientId): self
* @method static string getClientSecret(): string
*/ */
class BitinflowAccounts extends Facade class BitinflowAccounts extends Facade
{ {
protected static function getFacadeAccessor() protected static function getFacadeAccessor()
{ {
return BitinflowAccountsService::class; return BitinflowAccountsService::class;

View File

@@ -5,6 +5,7 @@ namespace Bitinflow\Accounts\Helpers;
use Firebase\JWT\JWT; use Firebase\JWT\JWT;
use Firebase\JWT\Key;
use Illuminate\Auth\AuthenticationException; use Illuminate\Auth\AuthenticationException;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use stdClass; use stdClass;
@@ -12,8 +13,6 @@ use Throwable;
class JwtParser class JwtParser
{ {
public const ALLOWED_ALGORITHMS = ['RS256'];
/** /**
* @param Request $request * @param Request $request
* @return stdClass * @return stdClass
@@ -26,8 +25,7 @@ class JwtParser
try { try {
return JWT::decode( return JWT::decode(
$request->bearerToken(), $request->bearerToken(),
$this->getOauthPublicKey(), new Key($this->getOauthPublicKey(),'RS256')
self::ALLOWED_ALGORITHMS
); );
} catch (Throwable $exception) { } catch (Throwable $exception) {
throw (new AuthenticationException()); throw (new AuthenticationException());

View File

@@ -10,13 +10,13 @@ use Bitinflow\Accounts\BitinflowAccounts;
use Bitinflow\Accounts\Helpers\JwtParser; use Bitinflow\Accounts\Helpers\JwtParser;
use Bitinflow\Accounts\Contracts; use Bitinflow\Accounts\Contracts;
use Bitinflow\Accounts\Repository; use Bitinflow\Accounts\Repository;
use Bitinflow\Payments\BitinflowPayments;
use Illuminate\Auth\RequestGuard; use Illuminate\Auth\RequestGuard;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\ServiceProvider; use Illuminate\Support\ServiceProvider;
class BitinflowAccountsServiceProvider extends ServiceProvider class BitinflowAccountsServiceProvider extends ServiceProvider
{ {
/** /**
* Bootstrap the application services. * Bootstrap the application services.
* *
@@ -41,6 +41,9 @@ class BitinflowAccountsServiceProvider extends ServiceProvider
$this->app->singleton(BitinflowAccounts::class, function () { $this->app->singleton(BitinflowAccounts::class, function () {
return new BitinflowAccounts; return new BitinflowAccounts;
}); });
$this->app->singleton(BitinflowPayments::class, function () {
return new BitinflowPayments;
});
$this->registerGuard(); $this->registerGuard();
} }
@@ -85,6 +88,9 @@ class BitinflowAccountsServiceProvider extends ServiceProvider
*/ */
public function provides() public function provides()
{ {
return [BitinflowAccounts::class]; return [
BitinflowAccounts::class,
BitinflowPayments::class,
];
} }
} }

View File

@@ -262,4 +262,9 @@ class Result
{ {
return $this->response; return $this->response;
} }
public function dump()
{
dump($this->data());
}
} }

View File

@@ -22,7 +22,7 @@ class Provider extends AbstractProvider implements ProviderInterface
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
protected $scopes = [Scope::READ_USER]; protected $scopes = [Scope::USER_READ];
/** /**
* {@inherticdoc}. * {@inherticdoc}.
@@ -53,14 +53,14 @@ class Provider extends AbstractProvider implements ProviderInterface
protected function getUserByToken($token) protected function getUserByToken($token)
{ {
$response = $this->getHttpClient()->get( $response = $this->getHttpClient()->get(
'https://accounts.bitinflow.com/api/user', [ 'https://accounts.bitinflow.com/api/v3/user', [
'headers' => [ 'headers' => [
'Accept' => 'application/json', 'Accept' => 'application/json',
'Authorization' => 'Bearer ' . $token, 'Authorization' => 'Bearer ' . $token,
], ],
]); ]);
return json_decode($response->getBody()->getContents(), true); return json_decode($response->getBody()->getContents(), true)['data'];
} }
/** /**
@@ -86,4 +86,4 @@ class Provider extends AbstractProvider implements ProviderInterface
'grant_type' => 'authorization_code', 'grant_type' => 'authorization_code',
]); ]);
} }
} }

View File

@@ -1,57 +0,0 @@
<?php
namespace Bitinflow\Accounts\Traits\BitinflowPaymentsWallet;
use App\Models\User;
class Balance
{
public function __construct(protected User $user)
{
//
}
/**
* Get balance from user.
*
* @return float|null
*/
public function get(): ?float
{
return $this->user->getPaymentsUser()->data->balance;
}
/**
* Deposit given amount from bank to account.
*
* @param float $amount
* @param string $description
* @return bool
*/
public function deposit(float $amount, string $decription): bool
{
$this->user->asPaymentsUser('PUT', sprintf('wallet/deposit', [
'amount' => $amount,
'decription' => $decription,
]));
}
/**
* Charge given amount from account.
*
* @param float $amount
* @param string $decription
* @return bool
*/
public function charge(float $amount, string $decription): bool
{
$order = $this->user->orders()->create([
'name' => $decription,
'description' => 'one-time payment',
'amount' => 1,
'price' => $amount,
]);
return $this->user->orders()->checkout($order->id);
}
}

View File

@@ -1,33 +0,0 @@
<?php
namespace Bitinflow\Accounts\Traits\BitinflowPaymentsWallet;
use App\Models\User;
class CheckoutSessions
{
public function __construct(protected User $user)
{
//
}
public function create(array $payload)
{
return $this->user->asPaymentsUser('POST', 'checkout-sessions', $payload);
}
public function get(string $id)
{
return $this->user->asPaymentsUser('GET', sprintf('checkout-sessions/%s', $id));
}
public function checkout(string $id)
{
return $this->user->asPaymentsUser('PUT', sprintf('checkout-sessions/%s/checkout', $id));
}
public function revoke(string $id)
{
return $this->user->asPaymentsUser('PUT', sprintf('checkout-sessions/%s/revoke', $id));
}
}

View File

@@ -1,69 +0,0 @@
<?php
namespace Bitinflow\Accounts\Traits\BitinflowPaymentsWallet;
use App\Models\User;
class Orders
{
public function __construct(protected User $user)
{
//
}
/**
* Get orders from user.
*
* @return object|null
*/
public function all(): ?object
{
return $this->user->asPaymentsUser('GET', 'orders');
}
/**
* @param string $id
* @return object|null
*/
public function get(string $id): ?object
{
return $this->user->asPaymentsUser('GET', sprintf('orders/%s', $id));
}
/**
* Create a new order.
*
* @param array $attributes
* @return object|false
*/
public function create(array $attributes = []): object|false
{
return $this->user->asPaymentsUser('POST', 'orders', $attributes)->data;
}
/**
* Checkout given subscription.
*
* @param string $id
* @return bool
*/
public function checkout(string $id): bool
{
$this->user->asPaymentsUser('PUT', sprintf('orders/%s/checkout', $id));
return true;
}
/**
* Revoke a running subscription.
*
* @param string $id
* @return bool
*/
public function revoke(string $id): bool
{
$this->user->asPaymentsUser('PUT', sprintf('orders/%s/revoke', $id));
return true;
}
}

View File

@@ -1,97 +0,0 @@
<?php
namespace Bitinflow\Accounts\Traits\BitinflowPaymentsWallet;
use App\Models\User;
class Subscriptions
{
public function __construct(protected User $user)
{
//
}
/**
* Get subscriptions from user.
*
* @return object|null
*/
public function all(): ?object
{
return $this->user->asPaymentsUser('GET', 'subscriptions');
}
/**
* @param string $id
* @return object|null
*/
public function get(string $id): ?object
{
return $this->user->asPaymentsUser('GET', sprintf('subscriptions/%s', $id));
}
/**
* Create a new subscription.
*
* @param array $attributes array which requires following attributes:
* name, description, period, price
* and following attributes are optional:
* vat, payload, ends_at, webhook_url, webhook_secret
* @return object|false the subscription object
* @throws GuzzleException
*/
public function create(array $attributes): object|false
{
return $this->user->asPaymentsUser('POST', 'subscriptions', $attributes)->data;
}
/**
* @param string $name
* @return bool
*/
public function has(string $name = 'default'): bool
{
$subscription = $this->get($name);
return $subscription && $subscription->status === 'settled' || $subscription && $subscription->resumeable;
}
/**
* Checkout given subscription.
*
* @param string $id
* @return bool
*/
public function checkout(string $id): bool
{
$this->user->asPaymentsUser('PUT', sprintf('subscriptions/%s/checkout', $id));
return true;
}
/**
* Revoke a running subscription.
*
* @param string $id
* @return bool
*/
public function revoke(string $id): bool
{
$this->user->asPaymentsUser('PUT', sprintf('subscriptions/%s/revoke', $id));
return true;
}
/**
* Resume a running subscription.
*
* @param string $id
* @return bool
*/
public function resume(string $id): bool
{
$this->user->asPaymentsUser('PUT', sprintf('subscriptions/%s/resume', $id));
return true;
}
}

View File

@@ -1,23 +0,0 @@
<?php
namespace Bitinflow\Accounts\Traits\BitinflowPaymentsWallet;
use App\Models\User;
class Taxation
{
public function __construct(protected User $user)
{
//
}
/**
* Get vat from user.
*
* @return int|null
*/
public function get(): ?int
{
return $this->user->getPaymentsUser()->data->taxation->vat;
}
}

View File

@@ -1,54 +0,0 @@
<?php
namespace Bitinflow\Accounts\Traits\BitinflowPaymentsWallet;
use App\Models\User;
class Wallets
{
public function __construct(protected User $user)
{
//
}
/**
* Get all wallets that belongs to the user.
*
* @return array|null
*/
public function all(): ?array
{
return $this->user->getPaymentsUser()->data->wallets;
}
/**
* Check if user has an active wallet.
*
* @return bool
* @throws GuzzleException
*/
public function has(): ?bool
{
return $this->user->getPaymentsUser()->data->has_wallet;
}
/**
* Set default wallet to given wallet token.
*
* @param string $token default payment method token
* @return bool
*/
public function setDefault(string $token): bool
{
$this->user->asPaymentsUser('PUT', sprintf('wallets/default', [
'token' => $token
]));
return true;
}
public function setupIntent()
{
return sprintf('%swallet/?continue_url=%s', config('bitinflow-accounts.payments.dashboard_url'), urlencode(url()->to($success_path)));
}
}

View File

@@ -1,99 +0,0 @@
<?php
namespace Bitinflow\Accounts\Traits;
use Bitinflow\Accounts\Traits\BitinflowPaymentsWallet\Balance;
use Bitinflow\Accounts\Traits\BitinflowPaymentsWallet\CheckoutSessions;
use Bitinflow\Accounts\Traits\BitinflowPaymentsWallet\Orders;
use Bitinflow\Accounts\Traits\BitinflowPaymentsWallet\Subscriptions;
use Bitinflow\Accounts\Traits\BitinflowPaymentsWallet\Taxation;
use Bitinflow\Accounts\Traits\BitinflowPaymentsWallet\Wallets;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
use GuzzleHttp\RequestOptions;
use Illuminate\Http\Client\PendingRequest;
/**
* @property Balance balance
* @property CheckoutSessions checkout_sessions
* @property Orders orders
* @property Subscriptions subscriptions
* @property Taxation taxation
* @property Wallets wallets
*/
trait HasBitinflowPaymentsWallet
{
protected $paymentsUser = null;
/**
* Create a new payment gateway request.
*
* @param string $method
* @param string $url
* @param array $attributes
* @return mixed
* @throws GuzzleException
*/
public function asPaymentsUser(string $method, string $url, array $attributes = []): mixed
{
$client = new Client([
'base_uri' => config('bitinflow-accounts.payments.base_url'),
]);
$response = $client->request($method, $url, [
RequestOptions::JSON => $attributes,
RequestOptions::HEADERS => [
'Accept' => 'application/json',
'Content-Type' => 'application/json',
'Authorization' => sprintf('Bearer %s', $this->getAttribute(config('auth.providers.sso-users.access_token_field'))),
],
]);
return json_decode($response->getBody());
}
/**
* Get user from payments gateway.
*
* @return object|null
* @throws GuzzleException
*/
public function getPaymentsUser(): ?object
{
if (is_null($this->paymentsUser)) {
$this->paymentsUser = $this->asPaymentsUser('GET', 'user');
}
return $this->paymentsUser;
}
public function getBalanceAttribute(): Balance
{
return new Balance($this);
}
public function getCheckoutSessionsAttribute(): CheckoutSessions
{
return new CheckoutSessions($this);
}
public function getOrdersAttribute(): Orders
{
return new Orders($this);
}
public function getSubscriptionsAttribute(): Subscriptions
{
return new Subscriptions($this);
}
public function getTaxationAttribute(): Taxation
{
return new Taxation($this);
}
public function getWalletsAttribute(): Wallets
{
return new Wallets($this);
}
}

View File

@@ -19,7 +19,7 @@ trait UsersTrait
*/ */
public function getAuthedUser(): Result public function getAuthedUser(): Result
{ {
return $this->get('users/me'); return $this->get('v3/user');
} }
/** /**
@@ -31,7 +31,7 @@ trait UsersTrait
*/ */
public function createUser(array $parameters): Result public function createUser(array $parameters): Result
{ {
return $this->post('v2/users', $parameters); return $this->post('v3/users', $parameters);
} }
/** /**
@@ -43,7 +43,7 @@ trait UsersTrait
*/ */
public function isEmailExisting(string $email): Result public function isEmailExisting(string $email): Result
{ {
return $this->post('v2/users/check-email', [ return $this->post('v3/users/check', [
'email' => $email, 'email' => $email,
]); ]);
} }

View File

@@ -0,0 +1,260 @@
<?php /** @noinspection DuplicatedCode */
namespace Bitinflow\Payments;
use Bitinflow\Accounts\Exceptions\RequestRequiresAuthenticationException;
use Bitinflow\Accounts\Exceptions\RequestRequiresClientIdException;
use Bitinflow\Accounts\Helpers\Paginator;
use Bitinflow\Payments\Result;
use Bitinflow\Accounts\ApiOperations;
use Bitinflow\Payments\Traits;
use Bitinflow\Support\Query;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
use GuzzleHttp\Exception\RequestException;
class BitinflowPayments
{
use Traits\Balance;
use Traits\Wallets;
use Traits\Orders;
use Traits\Subscriptions;
use Traits\CheckoutSessions;
use Traits\Taxation;
use ApiOperations\Validation;
private static string $baseUrl = 'https://api.pay.bitinflow.com/v1/';
/**
* bitinflow Payments OAuth token.
*/
protected ?string $token = null;
/**
* bitinflow Accounts client id.
*/
protected ?string $clientId = null;
/**
* bitinflow Accounts client secret.
*/
protected ?string $clientSecret = null;
private Client $client;
/**
* Constructor.
*/
public function __construct()
{
if ($clientId = config('bitinflow-accounts.client_id')) {
$this->setClientId($clientId);
}
if ($clientSecret = config('bitinflow-accounts.client_secret')) {
$this->setClientSecret($clientSecret);
}
if ($redirectUri = config('bitinflow-accounts.payments.base_url')) {
self::setBaseUrl($redirectUri);
}
$this->client = new Client([
'base_uri' => self::$baseUrl,
]);
}
/**
* @param string $baseUrl
*
* @internal only for internal and debug purposes.
*/
public static function setBaseUrl(string $baseUrl): void
{
self::$baseUrl = $baseUrl;
}
/**
* Get OAuth token.
*
* @return string bitinflow Accounts token
* @return string|null
* @throws RequestRequiresAuthenticationException
*/
public function getToken(): ?string
{
if (!$this->token) {
throw new RequestRequiresAuthenticationException;
}
return $this->token;
}
/**
* Set OAuth token.
*
* @param string $token bitinflow Accounts OAuth token
*
* @return void
*/
public function setToken(string $token): void
{
$this->token = $token;
}
/**
* Fluid OAuth token setter.
*
* @param string $token bitinflow Accounts OAuth token
*
* @return self
*/
public function withToken(string $token): self
{
$this->setToken($token);
return $this;
}
/**
* Build query & execute.
*
* @param string $method HTTP method
* @param string $path Query path
* @param array $parameters Query parameters
* @param Paginator|null $paginator Paginator object
* @param mixed|null $jsonBody JSON data
*
* @return Result Result object
* @throws GuzzleException
* @throws RequestRequiresClientIdException
*/
public function query(string $method = 'GET', string $path = '', array $parameters = [], Paginator $paginator = null, $jsonBody = null): Result
{
if ($paginator !== null) {
$parameters[$paginator->action] = $paginator->cursor();
}
try {
$response = $this->client->request($method, $path, [
'headers' => $this->buildHeaders((bool)$jsonBody),
'query' => Query::build($parameters),
'json' => $jsonBody ?: null,
]);
$result = new Result($response, null, $paginator);
} catch (RequestException $exception) {
$result = new Result($exception->getResponse(), $exception, $paginator);
}
$result->bitinflow = $this;
return $result;
}
/**
* Build headers for request.
*
* @param bool $json Body is JSON
*
* @return array
* @throws RequestRequiresClientIdException
*/
private function buildHeaders(bool $json = false): array
{
$headers = [
'Client-ID' => $this->getClientId(),
'Accept' => 'application/json',
];
if ($this->token) {
$headers['Authorization'] = 'Bearer ' . $this->token;
}
if ($json) {
$headers['Content-Type'] = 'application/json';
}
return $headers;
}
/**
* Get client id.
*
* @return string
* @throws RequestRequiresClientIdException
*/
public function getClientId(): string
{
if (!$this->clientId) {
throw new RequestRequiresClientIdException;
}
return $this->clientId;
}
/**
* Set client id.
*
* @param string $clientId bitinflow Accounts client id
*
* @return void
*/
public function setClientId(string $clientId): void
{
$this->clientId = $clientId;
}
/**
* Fluid client id setter.
*
* @param string $clientId bitinflow Accounts client id.
*
* @return self
*/
public function withClientId(string $clientId): self
{
$this->setClientId($clientId);
return $this;
}
/**
* Get client secret.
*
* @return string
* @throws RequestRequiresClientIdException
*/
public function getClientSecret(): string
{
if (!$this->clientSecret) {
throw new RequestRequiresClientIdException;
}
return $this->clientSecret;
}
/**
* Set client secret.
*
* @param string $clientSecret bitinflow Accounts client secret
*
* @return void
*/
public function setClientSecret(string $clientSecret): void
{
$this->clientSecret = $clientSecret;
}
/**
* Fluid client secret setter.
*
* @param string $clientSecret bitinflow Accounts client secret
*
* @return self
*/
public function withClientSecret(string $clientSecret): self
{
$this->setClientSecret($clientSecret);
return $this;
}
public function getBaseUrl()
{
return self::$baseUrl;
}
}

View File

@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace Bitinflow\Payments\Facades;
use Bitinflow\Payments\BitinflowPayments as BitinflowPaymentsService;
use Illuminate\Support\Facades\Facade;
/**
* @author René Preuß <rene@preuss.io>
*/
class BitinflowPayments extends Facade
{
protected static function getFacadeAccessor()
{
return BitinflowPaymentsService::class;
}
}

10
src/Payments/Result.php Normal file
View File

@@ -0,0 +1,10 @@
<?php
namespace Bitinflow\Payments;
use Bitinflow\Accounts\Result as Base;
class Result extends Base
{
}

View File

@@ -0,0 +1,45 @@
<?php
namespace Bitinflow\Payments\Traits;
use Bitinflow\Payments\Result;
use GuzzleHttp\Exception\GuzzleException;
trait Balance
{
/**
* Get balance from user.
*/
public function getUser(): Result
{
return $this->query('GET', 'user');
}
/**
* Deposit given amount from bank to account.
*/
public function deposit(float $amount, string $description): Result
{
return $this->query('PUT', 'wallet/deposit', [], null, [
'amount' => $amount,
'description' => $description,
]);
}
/**
* Charge given amount from account.
*
* @throws GuzzleException
*/
public function charge(float $amount, string $description): bool
{
$result = $this->createOrder([
'name' => $description,
'description' => 'one-time payment',
'amount' => 1,
'price' => $amount,
]);
return $this->checkoutOrder($result->data()->id);
}
}

View File

@@ -0,0 +1,46 @@
<?php
namespace Bitinflow\Payments\Traits;
use Bitinflow\Accounts\Exceptions\RequestRequiresClientIdException;
use Bitinflow\Payments\Result;
use GuzzleHttp\Exception\GuzzleException;
trait CheckoutSessions
{
/**
* @throws GuzzleException
* @throws RequestRequiresClientIdException
*/
public function createCheckoutSession(array $parameters): Result
{
return $this->query('POST', 'checkout-sessions', [], null, $parameters);
}
/**
* @throws GuzzleException
* @throws RequestRequiresClientIdException
*/
public function getCheckoutSession(string $id): Result
{
return $this->query('GET', sprintf('checkout-sessions/%s', $id));
}
/**
* @throws GuzzleException
* @throws RequestRequiresClientIdException
*/
public function checkoutCheckoutSession(string $id): Result
{
return $this->query('PUT', sprintf('checkout-sessions/%s/checkout', $id));
}
/**
* @throws GuzzleException
* @throws RequestRequiresClientIdException
*/
public function revokeCheckoutSession(string $id): Result
{
return $this->query('PUT', sprintf('checkout-sessions/%s/revoke', $id));
}
}

View File

@@ -0,0 +1,58 @@
<?php
namespace Bitinflow\Payments\Traits;
use Bitinflow\Payments\Result;
use Bitinflow\Payments\BitinflowPayments;
use GuzzleHttp\Exception\GuzzleException;
trait Orders
{
/**
* Get orders from user.
*
* @throws GuzzleException
*/
public function getOrders(): Result
{
return $this->query('GET', 'orders');
}
/**
* @param string $id
*/
public function getOrder(string $id): Result
{
return $this->query('GET', sprintf('orders/%s', $id));
}
/**
* Create a new order.
*
* @throws GuzzleException
*/
public function createOrder(array $parameters = []): Result
{
return $this->query('POST', 'orders', [], null, $parameters);
}
/**
* Checkout given subscription.
*
* @param string $id
*/
public function checkoutOrder(string $id):Result
{
return $this->query('PUT', sprintf('orders/%s/checkout', $id));
}
/**
* Revoke a running subscription.
*
* @param string $id
*/
public function revokeOrder(string $id):Result
{
return $this->query('PUT', sprintf('orders/%s/revoke', $id));
}
}

View File

@@ -0,0 +1,82 @@
<?php
namespace Bitinflow\Payments\Traits;
use Bitinflow\Accounts\Exceptions\RequestRequiresClientIdException;
use Bitinflow\Accounts\Exceptions\RequestRequiresMissingParametersException;
use Bitinflow\Accounts\Helpers\Paginator;
use Bitinflow\Payments\Result;
use GuzzleHttp\Exception\GuzzleException;
trait Subscriptions
{
/**
* Get subscriptions from user.
*
* @return object|null
*/
public function getSubscriptions(array $parameters = [], Paginator $paginator = null): Result
{
return $this->query('GET', 'subscriptions', $parameters, $paginator);
}
/**
* @param string $id
*/
public function getSubscription(string $id): Result
{
return $this->query('GET', sprintf('subscriptions/%s', $id));
}
/**
* Create a new subscription.
*
* @param array $parameters array which requires following attributes:
* name, description, period, price
* and following attributes are optional:
* vat, payload, ends_at, webhook_url, webhook_secret
* @return object|false the subscription object
* @throws GuzzleException
* @throws RequestRequiresClientIdException
* @throws RequestRequiresMissingParametersException
*/
public function createSubscription(array $parameters): Result
{
$this->validateRequired($parameters, ['name', 'description', 'period', 'price']);
return $this->query('POST', 'subscriptions', [], null, $parameters);
}
/**
* Force given subscription to check out (trusted apps only).
*
* @param string $id
* @return Result
* @throws RequestRequiresClientIdException
* @throws GuzzleException
*/
public function checkoutSubscription(string $id): Result
{
return $this->query('PUT', sprintf('subscriptions/%s/checkout', $id));
}
/**
* Revoke a running subscription.
*
* @param string $id
*/
public function revokeSubscription(string $id): Result
{
return $this->query('PUT', sprintf('subscriptions/%s/revoke', $id));
}
/**
* Resume a running subscription.
*
* @param string $id
*/
public function resumeSubscription(string $id): Result
{
return $this->query('PUT', sprintf('subscriptions/%s/resume', $id));
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace Bitinflow\Payments\Traits;
use Bitinflow\Payments\Result;
trait Taxation
{
/**
* Get vat from user.
*/
public function getTaxation(): Result
{
return $this->query('GET', 'taxation');
}
}

View File

@@ -0,0 +1,34 @@
<?php
namespace Bitinflow\Payments\Traits;
use Bitinflow\Payments\Result;
use Bitinflow\Payments\BitinflowPayments;
trait Wallets
{
/**
* Get all wallets that belong to the user.
*/
public function getWallets(): Result
{
return $this->query('GET', 'wallets');
}
/**
* Set default wallet to given wallet token.
*
* @param string $token default payment method token
*/
public function setDefaultWallet(string $token): Result
{
return $this->query('PUT', 'wallets/default', [], null, [
'token' => $token
]);
}
public function getWalletSetupIntent(string $successUrl): string
{
return sprintf('%swallet/?continue_url=%s', config('bitinflow-accounts.payments.dashboard_url'), urlencode($successUrl));
}
}

26
src/Support/Query.php Normal file
View File

@@ -0,0 +1,26 @@
<?php
namespace Bitinflow\Support;
class Query
{
/**
* Build query with support for multiple same first-dimension keys.
*
* @param array $query
*
* @return string
*/
public static function build(array $query): string
{
$parts = [];
foreach ($query as $name => $value) {
$value = (array)$value;
array_walk_recursive($value, function ($value) use (&$parts, $name) {
$parts[] = urlencode($name) . '=' . urlencode($value);
});
}
return implode('&', $parts);
}
}

View File

@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace Bitinflow\Payments\Tests;
use Bitinflow\Accounts\Contracts\AppTokenRepository;
use Bitinflow\Payments\Tests\TestCases\ApiTestCase;
/**
* @author René Preuß <rene@preuss.io>
*/
class ApiOauthTest extends ApiTestCase
{
public function testGetOauthToken(): void
{
$token = app(AppTokenRepository::class)->getAccessToken();
$this->getPaymentsClient()->withToken($this->getToken());
$this->registerResult($result = $this->getPaymentsClient()->createSubscription([
]));
$result->dump();
}
}

View File

@@ -0,0 +1,80 @@
<?php
declare(strict_types=1);
namespace Bitinflow\Payments\Tests\TestCases;
use Bitinflow\Accounts\BitinflowAccounts;
use Bitinflow\Accounts\Result;
use Bitinflow\Payments\BitinflowPayments;
/**
* @author René Preuß <rene@preuss.io>
*/
abstract class ApiTestCase extends TestCase
{
protected static $rateLimitRemaining = null;
protected function setUp(): void
{
parent::setUp();
if ($this->getAccountsBaseUrl()) {
BitinflowAccounts::setBaseUrl($this->getAccountsBaseUrl());
}
if ($this->getPaymentsBaseUrl()) {
BitinflowPayments::setBaseUrl($this->getPaymentsBaseUrl());
}
if (!$this->getClientId()) {
$this->markTestSkipped('No Client-ID given');
}
if (self::$rateLimitRemaining !== null && self::$rateLimitRemaining < 3) {
$this->markTestSkipped('Rate Limit exceeded (' . self::$rateLimitRemaining . ')');
}
$this->getAccountsClient()->setClientId($this->getClientId());
$this->getPaymentsClient()->setClientId($this->getClientId());
}
protected function registerResult(Result $result): Result
{
self::$rateLimitRemaining = $result->rateLimit('remaining');
return $result;
}
protected function getAccountsBaseUrl()
{
return getenv('ACCOUNTS_BASE_URL');
}
protected function getPaymentsBaseUrl()
{
return getenv('PAYMENTS_BASE_URL');
}
protected function getClientId()
{
return getenv('CLIENT_ID');
}
protected function getClientSecret()
{
return getenv('CLIENT_KEY');
}
protected function getToken()
{
return getenv('CLIENT_ACCESS_TOKEN');
}
public function getPaymentsClient(): BitinflowPayments
{
return app(BitinflowPayments::class);
}
public function getAccountsClient(): BitinflowAccounts
{
return app(BitinflowAccounts::class);
}
}

View File

@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Bitinflow\Payments\Tests\TestCases;
use Bitinflow\Accounts\BitinflowAccounts;
use Bitinflow\Accounts\Providers\BitinflowAccountsServiceProvider;
use Orchestra\Testbench\TestCase as BaseTestCase;
/**
* @author René Preuß <rene@preuss.io>
*/
abstract class TestCase extends BaseTestCase
{
protected function getPackageProviders($app)
{
return [
BitinflowAccountsServiceProvider::class,
];
}
protected function getPackageAliases($app)
{
return [
'BitinflowAccounts' => BitinflowAccounts::class,
];
}
}