mirror of
https://github.com/wearepixel/laravel-quickbooks.git
synced 2026-11-14 14:01:46 +00:00
Initial commit
This commit is contained in:
+259
@@ -0,0 +1,259 @@
|
||||
<?php
|
||||
|
||||
namespace Wearepixel\QuickBooks;
|
||||
|
||||
use Exception;
|
||||
use QuickBooksOnline\API\DataService\DataService;
|
||||
use QuickBooksOnline\API\Exception\SdkException;
|
||||
use QuickBooksOnline\API\Exception\ServiceException;
|
||||
use QuickBooksOnline\API\ReportService\ReportService;
|
||||
|
||||
/**
|
||||
* Class Client
|
||||
*/
|
||||
class Client
|
||||
{
|
||||
/**
|
||||
* The configs to set up a DataService
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $configs;
|
||||
|
||||
/**
|
||||
* The DataService instance
|
||||
*
|
||||
* @var DataService
|
||||
*/
|
||||
protected $data_service;
|
||||
|
||||
/**
|
||||
* The ReportService instance
|
||||
*
|
||||
* @var ReportService
|
||||
*/
|
||||
protected $report_service;
|
||||
|
||||
/**
|
||||
* The Token instance
|
||||
*
|
||||
* @var Token
|
||||
*/
|
||||
protected $token;
|
||||
|
||||
/**
|
||||
* Client constructor.
|
||||
*/
|
||||
public function __construct(array $configs, Token $token)
|
||||
{
|
||||
$this->configs = $configs;
|
||||
|
||||
$this->setToken($token);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build URI to request authorization
|
||||
*
|
||||
* @throws SdkException
|
||||
* @throws ServiceException
|
||||
*/
|
||||
public function authorizationUri(): string
|
||||
{
|
||||
return $this->getDataService()
|
||||
->getOAuth2LoginHelper()
|
||||
->getAuthorizationCodeURL();
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the logging per config/quickbooks.php
|
||||
*/
|
||||
public function configureLogging(): DataService
|
||||
{
|
||||
// In case any of the keys are not in the configs, just disable logging
|
||||
try {
|
||||
if (
|
||||
$this->configs['logging']['enabled'] &&
|
||||
dir($this->configs['logging']['location'])
|
||||
) {
|
||||
$this->data_service->setLogLocation($this->configs['logging']['location']);
|
||||
|
||||
return $this->data_service->enableLog();
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
// TODO: Figure out what to do with this exception
|
||||
}
|
||||
|
||||
return $this->data_service->disableLog();
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the token
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function deleteToken(): self
|
||||
{
|
||||
$this->setToken($this->token->remove());
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert code to an access token
|
||||
*
|
||||
* Upon the user allowing access to their account, there is a code sent to
|
||||
* over that needs to be converted to an OAuth token.
|
||||
*
|
||||
* @throws SdkException
|
||||
* @throws ServiceException
|
||||
*/
|
||||
public function exchangeCodeForToken(string $code, int $realm_id): self
|
||||
{
|
||||
$oauth_token = $this->getDataService()
|
||||
->getOAuth2LoginHelper()
|
||||
->exchangeAuthorizationCodeForToken($code, $realm_id);
|
||||
|
||||
$this->getDataService()->updateOAuth2Token($oauth_token);
|
||||
|
||||
$this->token->parseOauthToken($oauth_token)->save();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Getter for the DataService
|
||||
*
|
||||
* Makes sure that it is setup & ready to be used.
|
||||
*
|
||||
* @throws SdkException
|
||||
* @throws ServiceException
|
||||
*/
|
||||
public function getDataService(): DataService
|
||||
{
|
||||
if (! $this->hasValidAccessToken() || ! isset($this->data_service)) {
|
||||
$this->data_service = $this->makeDataService();
|
||||
|
||||
$this->configureLogging();
|
||||
}
|
||||
|
||||
return $this->data_service;
|
||||
}
|
||||
|
||||
/**
|
||||
* Getter for the ReportService
|
||||
*
|
||||
* Makes sure that it is setup & ready to be used.
|
||||
*
|
||||
* @throws SdkException
|
||||
* @throws ServiceException
|
||||
*/
|
||||
public function getReportService(): ReportService
|
||||
{
|
||||
if (! $this->hasValidAccessToken() || ! isset($this->report_service)) {
|
||||
$this->report_service = new ReportService($this->getDataService()->getServiceContext());
|
||||
}
|
||||
|
||||
return $this->report_service;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check to see if the token has a valid access token
|
||||
*/
|
||||
public function hasValidAccessToken(): bool
|
||||
{
|
||||
return $this->token->hasValidAccessToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check to see if the token has a valid refresh token
|
||||
*/
|
||||
public function hasValidRefreshToken(): bool
|
||||
{
|
||||
return $this->token->hasValidRefreshToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory to make DataService
|
||||
*
|
||||
* There are 3 use cases for making a DataService....
|
||||
*
|
||||
* 1) Have valid access token, so ready to be used
|
||||
* 2) Have valid refresh token, so renew access token & then use
|
||||
* 3) No existing token, so need to link account
|
||||
*
|
||||
* @throws SdkException
|
||||
* @throws ServiceException
|
||||
*/
|
||||
protected function makeDataService(): DataService
|
||||
{
|
||||
// Associative array to use to filter out only the needed config keys when using existing token
|
||||
$existing_keys = [
|
||||
'auth_mode' => null,
|
||||
'baseUrl' => null,
|
||||
'ClientID' => null,
|
||||
'ClientSecret' => null,
|
||||
];
|
||||
|
||||
// Have good access & refresh, so allow app to run
|
||||
if ($this->hasValidAccessToken()) {
|
||||
// Pull in the configs from the token into needed keys from the configs
|
||||
return DataService::Configure(
|
||||
array_merge(array_intersect_key($this->parseDataConfigs(), $existing_keys), [
|
||||
'accessTokenKey' => $this->token->access_token,
|
||||
'QBORealmID' => $this->token->realm_id,
|
||||
'refreshTokenKey' => $this->token->refresh_token,
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
// Have refresh, so update access & allow app to run
|
||||
if ($this->hasValidRefreshToken()) {
|
||||
// Pull in the configs from the token into needed keys from the configs
|
||||
$data_service = DataService::Configure(
|
||||
array_merge(array_intersect_key($this->parseDataConfigs(), $existing_keys), [
|
||||
'QBORealmID' => $this->token->realm_id,
|
||||
'refreshTokenKey' => $this->token->refresh_token,
|
||||
]),
|
||||
);
|
||||
|
||||
$oauth_token = $data_service->getOAuth2LoginHelper()->refreshToken();
|
||||
|
||||
$data_service->updateOAuth2Token($oauth_token);
|
||||
|
||||
$this->token->parseOauthToken($oauth_token)->save();
|
||||
|
||||
return $data_service;
|
||||
}
|
||||
|
||||
// Create new...
|
||||
return DataService::Configure($this->parseDataConfigs());
|
||||
}
|
||||
|
||||
/**
|
||||
* QuickBooks is not consistent on their naming of variables, so map them
|
||||
*/
|
||||
protected function parseDataConfigs(): array
|
||||
{
|
||||
return [
|
||||
'auth_mode' => $this->configs['data_service']['auth_mode'],
|
||||
'baseUrl' => $this->configs['data_service']['base_url'],
|
||||
'ClientID' => $this->configs['data_service']['client_id'],
|
||||
'ClientSecret' => $this->configs['data_service']['client_secret'],
|
||||
'RedirectURI' => route('quickbooks.token'),
|
||||
'scope' => $this->configs['data_service']['scope'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Allow setting a token to switch "user"
|
||||
*/
|
||||
public function setToken(Token $token): self
|
||||
{
|
||||
$this->token = $token;
|
||||
|
||||
// The DataService is tied to a specific token, so remove it when using a new one
|
||||
unset($this->data_service);
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace Wearepixel\QuickBooks;
|
||||
|
||||
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||
|
||||
trait HasQuickBooksToken
|
||||
{
|
||||
/**
|
||||
* Have a quickBooksToken.
|
||||
*/
|
||||
public function quickBooksToken(): HasOne
|
||||
{
|
||||
return $this->hasOne(Token::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
namespace Wearepixel\QuickBooks\Http\Controllers;
|
||||
|
||||
use Exception;
|
||||
use Illuminate\Contracts\Routing\UrlGenerator;
|
||||
use Illuminate\Contracts\View\Factory as ViewFactory;
|
||||
use Illuminate\Contracts\View\View as ViewContract;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Routing\Controller as LaravelController;
|
||||
use Illuminate\Routing\Redirector;
|
||||
use Illuminate\Support\Facades\View;
|
||||
use QuickBooksOnline\API\Exception\SdkException;
|
||||
use QuickBooksOnline\API\Exception\ServiceException;
|
||||
use Wearepixel\QuickBooks\Client as QuickBooks;
|
||||
|
||||
/**
|
||||
* Class Controller
|
||||
*/
|
||||
class Controller extends LaravelController
|
||||
{
|
||||
// NOTE: When using constructor injection for QuickBooks, there is a race issue with the boot order of the app
|
||||
|
||||
/**
|
||||
* Form to connect/disconnect user to QuickBooks
|
||||
*
|
||||
* If the user has a valid OAuth, then give form to disconnect, otherwise link to connect it
|
||||
*
|
||||
* @throws SdkException
|
||||
* @throws ServiceException
|
||||
*/
|
||||
public function connect(QuickBooks $quickbooks, ViewFactory $view_factory): ViewContract|View
|
||||
{
|
||||
// Give view to remove token if user already linked account
|
||||
if ($quickbooks->hasValidRefreshToken()) {
|
||||
return $view_factory
|
||||
->make('quickbooks::disconnect')
|
||||
->with('company', $quickbooks->getDataService()->getCompanyInfo());
|
||||
}
|
||||
|
||||
// Give view to link account
|
||||
return $view_factory
|
||||
->make('quickbooks::connect')
|
||||
->with('authorization_uri', $quickbooks->authorizationUri());
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the token
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function disconnect(
|
||||
Redirector $redirector,
|
||||
Request $request,
|
||||
QuickBooks $quickbooks,
|
||||
): RedirectResponse|View {
|
||||
$quickbooks->deleteToken();
|
||||
|
||||
$request->session()->flash('success', 'Disconnected from QuickBooks');
|
||||
|
||||
return $redirector->back();
|
||||
}
|
||||
|
||||
/**
|
||||
* Accept the code from QuickBooks to request token
|
||||
*
|
||||
* Once a user approves linking account, then QuickBooks sends back
|
||||
* a code which can be converted to an OAuth token.
|
||||
*
|
||||
* @throws SdkException
|
||||
* @throws ServiceException
|
||||
*/
|
||||
public function token(
|
||||
Redirector $redirector,
|
||||
Request $request,
|
||||
QuickBooks $quickbooks,
|
||||
UrlGenerator $url_generator,
|
||||
): RedirectResponse {
|
||||
// TODO: Deal with exceptions
|
||||
$quickbooks->exchangeCodeForToken($request->get('code'), $request->get('realmId'));
|
||||
|
||||
$request->session()->flash('success', 'Connected to QuickBooks');
|
||||
|
||||
return $redirector->intended($url_generator->route('quickbooks.connect'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace Wearepixel\QuickBooks\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Contracts\Routing\UrlGenerator;
|
||||
use Illuminate\Contracts\Session\Session;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Routing\Redirector;
|
||||
use Wearepixel\QuickBooks\Client as QuickBooks;
|
||||
|
||||
/**
|
||||
* Class Filter
|
||||
*/
|
||||
class Filter
|
||||
{
|
||||
/**
|
||||
* Create a new QuickBooks filter middleware instance.
|
||||
*/
|
||||
public function __construct(
|
||||
protected QuickBooks $quickbooks,
|
||||
protected Redirector $redirector,
|
||||
protected Session $session,
|
||||
protected UrlGenerator $url_generator,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*/
|
||||
public function handle(Request $request, Closure $next): mixed
|
||||
{
|
||||
if (! $this->quickbooks->hasValidRefreshToken()) {
|
||||
// Set intended route, so that after linking account, user is put where they were going
|
||||
$this->session->put('url.intended', $this->url_generator->to($request->path()));
|
||||
|
||||
return $this->redirector->route('quickbooks.connect');
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace Wearepixel\QuickBooks\Providers;
|
||||
|
||||
use Illuminate\Contracts\Foundation\Application;
|
||||
use Illuminate\Support\ServiceProvider as LaravelServiceProvider;
|
||||
use Wearepixel\QuickBooks\Client;
|
||||
|
||||
/**
|
||||
* Class ClientServiceProvider
|
||||
*/
|
||||
class ClientServiceProvider extends LaravelServiceProvider
|
||||
{
|
||||
/**
|
||||
* Indicates if loading of the provider is deferred.
|
||||
*/
|
||||
protected bool $defer = true;
|
||||
|
||||
/**
|
||||
* Get the services provided by the provider.
|
||||
*/
|
||||
public function provides(): array
|
||||
{
|
||||
return [Client::class];
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the application services.
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
$this->app->bind(Client::class, function (Application $app) {
|
||||
$token =
|
||||
$app->auth->user()->quickBooksToken ?:
|
||||
$app->auth
|
||||
->user()
|
||||
->quickBooksToken()
|
||||
->make();
|
||||
|
||||
return new Client($app->config->get('quickbooks'), $token);
|
||||
});
|
||||
|
||||
$this->app->alias(Client::class, 'QuickBooks');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
<?php
|
||||
|
||||
namespace Wearepixel\QuickBooks\Providers;
|
||||
|
||||
use Illuminate\Routing\Router;
|
||||
use Illuminate\Support\ServiceProvider as LaravelServiceProvider;
|
||||
use Wearepixel\QuickBooks\Http\Middleware\Filter;
|
||||
|
||||
/**
|
||||
* Class ServiceProvider
|
||||
*/
|
||||
class ServiceProvider extends LaravelServiceProvider
|
||||
{
|
||||
/**
|
||||
* Bootstrap the application services.
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
$this->registerMiddleware();
|
||||
|
||||
$this->registerPublishes();
|
||||
|
||||
$this->registerRoutes();
|
||||
|
||||
$this->registerViews();
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the application services.
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
$this->mergeConfigFrom(__DIR__.'/../../config/quickbooks.php', 'quickbooks');
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the middleware
|
||||
*
|
||||
* If a route needs to have the QuickBooks client, then make sure that the user has linked their account.
|
||||
*/
|
||||
public function registerMiddleware(): void
|
||||
{
|
||||
$this->app->router->aliasMiddleware('quickbooks', Filter::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* There are several resources that get published
|
||||
*
|
||||
* Only worry about telling the application about them if running in the console.
|
||||
*/
|
||||
protected function registerPublishes(): void
|
||||
{
|
||||
if ($this->app->runningInConsole()) {
|
||||
$this->loadMigrationsFrom(__DIR__.'/../../database/migrations');
|
||||
|
||||
$this->publishes(
|
||||
[
|
||||
__DIR__.'/../../config/quickbooks.php' => config_path('quickbooks.php'),
|
||||
],
|
||||
'quickbooks-config',
|
||||
);
|
||||
|
||||
$this->publishes(
|
||||
[
|
||||
__DIR__.'/../../database/migrations' => database_path('migrations'),
|
||||
],
|
||||
'quickbooks-migrations',
|
||||
);
|
||||
|
||||
$this->publishes(
|
||||
[
|
||||
__DIR__.'/../../resources/views' => base_path(
|
||||
'resources/views/vendor/quickbooks',
|
||||
),
|
||||
],
|
||||
'quickbooks-views',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the routes needed for the registration flow
|
||||
*/
|
||||
protected function registerRoutes(): void
|
||||
{
|
||||
$config = $this->app->config->get('quickbooks.route');
|
||||
|
||||
$this->app->router
|
||||
->prefix($config['prefix'])
|
||||
->as('quickbooks.')
|
||||
->middleware($config['middleware']['default'])
|
||||
->namespace('Wearepixel\QuickBooks\Http\Controllers')
|
||||
->group(function (Router $router) use ($config) {
|
||||
$router
|
||||
->get($config['paths']['connect'], 'Controller@connect')
|
||||
->middleware($config['middleware']['authenticated'])
|
||||
->name('connect');
|
||||
|
||||
$router
|
||||
->delete($config['paths']['disconnect'], 'Controller@disconnect')
|
||||
->middleware($config['middleware']['authenticated'])
|
||||
->name('disconnect');
|
||||
|
||||
$router->get($config['paths']['token'], 'Controller@token')->name('token');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the views
|
||||
*/
|
||||
protected function registerViews(): void
|
||||
{
|
||||
$this->loadViewsFrom(__DIR__.'/../../resources/views', 'quickbooks');
|
||||
}
|
||||
}
|
||||
+126
@@ -0,0 +1,126 @@
|
||||
<?php
|
||||
|
||||
namespace Wearepixel\QuickBooks;
|
||||
|
||||
use App\Models\User;
|
||||
use Carbon\Carbon;
|
||||
use Exception;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use QuickBooksOnline\API\Core\OAuth\OAuth2\OAuth2AccessToken;
|
||||
use QuickBooksOnline\API\Exception\SdkException;
|
||||
|
||||
/**
|
||||
* Class Token
|
||||
*
|
||||
*
|
||||
* @property bool $hasValidAccessToken Is the access token valid
|
||||
* @property bool $hasValidRefreshToken Is the refresh token valid
|
||||
* @property Carbon $access_token_expires_at Timestamp that the access token expires
|
||||
* @property Carbon $refresh_token_expires_at Timestamp that the refresh token expires
|
||||
* @property int $user_id Id of the related User
|
||||
* @property string $access_token The access token
|
||||
* @property string $realm_id Realm Id from the OAuth token
|
||||
* @property string $refresh_token The refresh token
|
||||
* @property User $user
|
||||
*/
|
||||
class Token extends Model
|
||||
{
|
||||
/**
|
||||
* The table associated with the model.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $table = 'quickbooks_tokens';
|
||||
|
||||
/**
|
||||
* The attributes that should be mutated to dates.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $dates = ['access_token_expires_at', 'refresh_token_expires_at'];
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $fillable = [
|
||||
'access_token',
|
||||
'access_token_expires_at',
|
||||
'realm_id',
|
||||
'refresh_token',
|
||||
'refresh_token_expires_at',
|
||||
'user_id',
|
||||
];
|
||||
|
||||
/**
|
||||
* Check if access token is valid
|
||||
*
|
||||
* A token is good for 1 hour, so if it expires greater than 1 hour from now, it is still valid
|
||||
*/
|
||||
public function getHasValidAccessTokenAttribute(): bool
|
||||
{
|
||||
return $this->access_token_expires_at && Carbon::now()->lt($this->access_token_expires_at);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if refresh token is valid
|
||||
*
|
||||
* A token is good for 101 days, so if it expires greater than 101 days from now, it is still valid
|
||||
*/
|
||||
public function getHasValidRefreshTokenAttribute(): bool
|
||||
{
|
||||
return $this->refresh_token_expires_at &&
|
||||
Carbon::now()->lt($this->refresh_token_expires_at);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse OauthToken.
|
||||
*
|
||||
* Process the OAuth token & store it in the persistent storage
|
||||
*
|
||||
* @throws SdkException
|
||||
*/
|
||||
public function parseOauthToken(OAuth2AccessToken $oauth_token): Token
|
||||
{
|
||||
// TODO: Deal with exception
|
||||
$this->access_token = $oauth_token->getAccessToken();
|
||||
$this->access_token_expires_at = Carbon::parse($oauth_token->getAccessTokenExpiresAt());
|
||||
$this->realm_id = $oauth_token->getRealmID();
|
||||
$this->refresh_token = $oauth_token->getRefreshToken();
|
||||
$this->refresh_token_expires_at = Carbon::parse($oauth_token->getRefreshTokenExpiresAt());
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the token
|
||||
*
|
||||
* When a token is deleted, we still need a token for the client for the user.
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function remove(): Token
|
||||
{
|
||||
$user = $this->user;
|
||||
|
||||
$this->delete();
|
||||
|
||||
return $user->quickBooksToken()->make();
|
||||
}
|
||||
|
||||
/**
|
||||
* Belongs to user.
|
||||
*/
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
$config = config('quickbooks.user');
|
||||
|
||||
return $this->belongsTo(
|
||||
$config['model'],
|
||||
$config['keys']['foreign'],
|
||||
$config['keys']['owner'],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user