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:
@@ -0,0 +1,16 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
trim_trailing_whitespace = false
|
||||||
|
|
||||||
|
[*.yml]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
* text=auto
|
||||||
|
|
||||||
|
/build export-ignore
|
||||||
|
/tests export-ignore
|
||||||
|
.gitattributes export-ignore
|
||||||
|
.gitignore export-ignore
|
||||||
|
.travis.yml export-ignore
|
||||||
|
nitpick.json export-ignore
|
||||||
|
phpunit.xml.dist export-ignore
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
/vendor
|
||||||
|
/node_modules
|
||||||
|
phpunit.xml
|
||||||
|
/build
|
||||||
|
.phpunit.result.cache
|
||||||
|
composer.lock
|
||||||
|
/.idea/
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
# Wearepixel's Laravel QuickBooks Client
|
||||||
|
|
||||||
|
[](https://packagist.org/packages/wearepixel/laravel-quickbooks-client)
|
||||||
|
[](https://packagist.org/packages/wearepixel/laravel-quickbooks-client)
|
||||||
|
[](https://packagist.org/packages/wearepixel/laravel-quickbooks-client)
|
||||||
|
[](https://packagist.org/packages/wearepixel/laravel-quickbooks-client)
|
||||||
|
|
||||||
|
PHP client wrapping the [QuickBooks PHP SDK](https://github.com/intuit/QuickBooks-V3-PHP-SDK).
|
||||||
|
|
||||||
|
We solely use [Laravel](https://www.laravel.com) for our applications, so this package is written with Laravel in mind. If there is a request from the community to split this package into 2 parts to allow it to be used outside of Laravel, then we will consider doing that work.
|
||||||
|
|
||||||
|
## Build Status
|
||||||
|
|
||||||
|
| Branch | Status | Coverage | Code Quality |
|
||||||
|
| ------ | :----: | :------: | :----------: |
|
||||||
|
| Develop | [](https://github.com/wearepixel/laravel-quickbooks-client/workflows/CI/badge.svg?branch=develop) | [](https://scrutinizer-ci.com/g/wearepixel/laravel-quickbooks-client/?branch=develop) | [](https://scrutinizer-ci.com/g/wearepixel/laravel-quickbooks-client/?branch=develop) |
|
||||||
|
| Master | [](https://github.com/wearepixel/laravel-quickbooks-client/workflows/CI/badge.svg?branch=master) | [](https://scrutinizer-ci.com/g/wearepixel/laravel-quickbooks-client/?branch=master) | [](https://scrutinizer-ci.com/g/wearepixel/laravel-quickbooks-client/?branch=master) |
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
1. Install QuickBooks PHP Client:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ composer require wearepixel/laravel-quickbooks-client
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Run our migration to install the `quickbooks_tokens` table:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ php artisan migrate --package=wearepixel/laravel-quickbooks-client
|
||||||
|
```
|
||||||
|
|
||||||
|
The package uses the [auto registration feature](https://laravel.com/docs/packages#package-discovery) of Laravel.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
1. You will need a ```quickBooksToken``` relationship on your ```User``` model. There is a trait named ```Wearepixel\QuickBooks\HasQuickBooksToken```, which you can include on your ```User``` model, which will setup the relationship. To do this implement the following:
|
||||||
|
|
||||||
|
Add ```use Wearepixel\QuickBooks\HasQuickBooksToken;``` to your service container at the top of User.php
|
||||||
|
and also add the trait within the class. For example:
|
||||||
|
|
||||||
|
```php
|
||||||
|
class User extends Authenticatable
|
||||||
|
{
|
||||||
|
use Notifiable, HasQuickBooksToken;
|
||||||
|
```
|
||||||
|
|
||||||
|
**NOTE: If your ```User``` model is not ```App\Models\User```, then you will need to configure the path in the ```configs/quickbooks.php```.**
|
||||||
|
|
||||||
|
2. Add the appropriate values to your ```.env```
|
||||||
|
|
||||||
|
#### Minimal Keys
|
||||||
|
```bash
|
||||||
|
QUICKBOOKS_CLIENT_ID=<client id given by QuickBooks>
|
||||||
|
QUICKBOOKS_CLIENT_SECRET=<client secret>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Optional Keys
|
||||||
|
```bash
|
||||||
|
QUICKBOOKS_API_URL=<Development|Production> # Defaults to App's env value
|
||||||
|
QUICKBOOKS_DEBUG=<true|false> # Defaults to App's debug value
|
||||||
|
```
|
||||||
|
|
||||||
|
3. _[Optional]_ Publish configs & views
|
||||||
|
|
||||||
|
#### Config
|
||||||
|
A configuration file named ```quickbooks.php``` can be published to ```config/``` by running...
|
||||||
|
|
||||||
|
```bash
|
||||||
|
php artisan vendor:publish --tag=quickbooks-config
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Views
|
||||||
|
View files can be published by running...
|
||||||
|
|
||||||
|
```bash
|
||||||
|
php artisan vendor:publish --tag=quickbooks-views
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Here is an example of getting the company information from QuickBooks:
|
||||||
|
|
||||||
|
### NOTE: Before doing these commands, go to your connect route (default: /quickbooks/connect) to get a QuickBooks token for your user
|
||||||
|
|
||||||
|
```php
|
||||||
|
php artisan tinker
|
||||||
|
Psy Shell v0.8.17 (PHP 7.1.14 — cli) by Justin Hileman
|
||||||
|
>>> Auth::logInUsingId(1)
|
||||||
|
=> App\Models\User {#1668
|
||||||
|
id: 1,
|
||||||
|
// Other keys removed for example
|
||||||
|
}
|
||||||
|
>>> $quickbooks = app('Wearepixel\QuickBooks\Client') // or app('QuickBooks')
|
||||||
|
=> Wearepixel\QuickBooks\Client {#1613}
|
||||||
|
>>> $quickbooks->getDataService()->getCompanyInfo();
|
||||||
|
=> QuickBooksOnline\API\Data\IPPCompanyInfo {#1673
|
||||||
|
+CompanyName: "Sandbox Company_US_1",
|
||||||
|
+LegalName: "Sandbox Company_US_1",
|
||||||
|
// Other properties removed for example
|
||||||
|
}
|
||||||
|
>>>
|
||||||
|
```
|
||||||
|
|
||||||
|
You can call any of the resources as documented [in the SDK](https://intuit.github.io/QuickBooks-V3-PHP-SDK/quickstart.html).
|
||||||
|
|
||||||
|
## Middleware
|
||||||
|
|
||||||
|
If you have routes that will be dependent on the user's account having a usable QuickBooks OAuth token, there is an included middleware ```Wearepixel\QuickBooks\Laravel\Filter``` that gets registered as ```quickbooks``` that will ensure the account is linked and redirect them to the `connect` route if needed.
|
||||||
|
|
||||||
|
Here is an example route definition:
|
||||||
|
|
||||||
|
```php
|
||||||
|
Route::view('some/route/needing/quickbooks/token/before/using', 'some.view')
|
||||||
|
->middleware('quickbooks');
|
||||||
|
```
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
{
|
||||||
|
"name": "wearepixel/laravel-quickbooks",
|
||||||
|
"description": "Wearepixel's Laravel Client for QuickBooks.",
|
||||||
|
"keywords": [
|
||||||
|
"quickbooks",
|
||||||
|
"client",
|
||||||
|
"laravel",
|
||||||
|
"wearepixel"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Joel Male",
|
||||||
|
"email": "joel.male@wearepixel.com"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"require": {
|
||||||
|
"php": "^8.0.2",
|
||||||
|
"illuminate/database": "^10.0|^11.0",
|
||||||
|
"illuminate/http": "^10.0|^11.0",
|
||||||
|
"illuminate/routing": "^10.0|^11.0",
|
||||||
|
"illuminate/support": "^10.0|^11.0",
|
||||||
|
"nesbot/carbon": "^2.62.1",
|
||||||
|
"quickbooks/v3-php-sdk": "^5.4.6"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"laravel/pint": "^1.2",
|
||||||
|
"mockery/mockery": "^1.5.1",
|
||||||
|
"phpunit/phpunit": "^10.0",
|
||||||
|
"psy/psysh": "^0.11",
|
||||||
|
"scrutinizer/ocular": "^1.9"
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Wearepixel\\QuickBooks\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload-dev": {
|
||||||
|
"psr-4": {
|
||||||
|
"Wearepixel\\QuickBooks\\": "tests/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"extra": {
|
||||||
|
"laravel": {
|
||||||
|
"providers": [
|
||||||
|
"Wearepixel\\QuickBooks\\Providers\\ServiceProvider",
|
||||||
|
"Wearepixel\\QuickBooks\\Providers\\ClientServiceProvider"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"sort-packages": true
|
||||||
|
},
|
||||||
|
"minimum-stability": "stable",
|
||||||
|
"prefer-stable": true
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Properties for the QuickBooks SDK DataService
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| The configuration keys for the SDK are inconsistent in naming convention.
|
||||||
|
| We are adhering to snake_case. We make a sensible guess for 'base_url'
|
||||||
|
| using the app's env, but you can can it with 'QUICKBOOKS_API_URL'. Also,
|
||||||
|
| the 'redirect_uri' is made in the client from the 'quickbooks.token'
|
||||||
|
| named route, so it cannot be configured here.
|
||||||
|
|
|
||||||
|
| Most of the time, only 'QUICKBOOKS_CLIENT_ID' & 'QUICKBOOKS_CLIENT_SECRET'
|
||||||
|
| needs to be set.
|
||||||
|
|
|
||||||
|
| See: https://intuit.github.io/QuickBooks-V3-PHP-SDK/configuration.html
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'data_service' => [
|
||||||
|
'auth_mode' => 'oauth2',
|
||||||
|
'base_url' => env('QUICKBOOKS_API_URL', config('app.env') === 'production' ? 'Production' : 'Development'),
|
||||||
|
'client_id' => env('QUICKBOOKS_CLIENT_ID'),
|
||||||
|
'client_secret' => env('QUICKBOOKS_CLIENT_SECRET'),
|
||||||
|
'scope' => 'com.intuit.quickbooks.accounting',
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Properties to control logging
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Configures logging to <storage_path>/logs/quickbooks.log when in debug
|
||||||
|
| mode or when 'QUICKBOOKS_DEBUG' is true.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'logging' => [
|
||||||
|
'enabled' => env('QUICKBOOKS_DEBUG', config('app.debug')),
|
||||||
|
|
||||||
|
'location' => storage_path('logs'),
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Properties to configure the routes
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| There are several routes that are needed for the package, so these
|
||||||
|
| properties allow configuring them to fit the application as needed.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'route' => [
|
||||||
|
// Controls the middlewares for thr routes. Can be a string or array of strings
|
||||||
|
'middleware' => [
|
||||||
|
// Added to the protected routes for the package (i.e. connect & disconnect)
|
||||||
|
'authenticated' => 'auth',
|
||||||
|
// Added to all of the routes for the package
|
||||||
|
'default' => 'web',
|
||||||
|
],
|
||||||
|
'paths' => [
|
||||||
|
// Show forms to connect/disconnect
|
||||||
|
'connect' => 'connect',
|
||||||
|
// The DELETE takes place to remove token
|
||||||
|
'disconnect' => 'disconnect',
|
||||||
|
// Return URI that QuickBooks sends code to allow getting OAuth token
|
||||||
|
'token' => 'token',
|
||||||
|
],
|
||||||
|
'prefix' => 'quickbooks',
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Properties for control the "user" relationship in Token
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| The Token class has a "user" relationship, and these properties allow
|
||||||
|
| configuring the relationship.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'user' => [
|
||||||
|
'keys' => [
|
||||||
|
'foreign' => 'user_id',
|
||||||
|
'owner' => 'id',
|
||||||
|
],
|
||||||
|
'model' => User::class,
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
Schema::create('quickbooks_tokens', function (Blueprint $table) {
|
||||||
|
$user_id_type =
|
||||||
|
DB::getSchemaBuilder()->getColumnType('users', 'id') === 'bigint'
|
||||||
|
? 'unsignedBigInteger'
|
||||||
|
: 'unsignedInteger';
|
||||||
|
|
||||||
|
$table->bigIncrements('id');
|
||||||
|
$table->{$user_id_type}('user_id');
|
||||||
|
$table->unsignedBigInteger('realm_id');
|
||||||
|
$table->longtext('access_token');
|
||||||
|
$table->datetime('access_token_expires_at');
|
||||||
|
$table->string('refresh_token');
|
||||||
|
$table->datetime('refresh_token_expires_at');
|
||||||
|
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
$table
|
||||||
|
->foreign('user_id')
|
||||||
|
->references('id')
|
||||||
|
->on('users')
|
||||||
|
->onDelete('cascade');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('quickbooks_tokens');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
* text=auto
|
||||||
|
|
||||||
|
/build export-ignore
|
||||||
|
/tests export-ignore
|
||||||
|
.gitattributes export-ignore
|
||||||
|
.gitignore export-ignore
|
||||||
|
.travis.yml export-ignore
|
||||||
|
nitpick.json export-ignore
|
||||||
|
phpunit.xml.dist export-ignore
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
This is the "dist(ribution)" phpunit.xml.dist file. It sets the defaults that are then over written by any files in
|
||||||
|
phpunit.xml, which is then over wrote by flags passed in via the command line. The plan is that this file is to be
|
||||||
|
used by ci to do the full suit of tests, and a developer can copy this file to phpunit.xml to trim down some of the
|
||||||
|
options.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<phpunit backupGlobals="false"
|
||||||
|
backupStaticAttributes="false"
|
||||||
|
bootstrap="vendor/autoload.php"
|
||||||
|
colors="true"
|
||||||
|
convertErrorsToExceptions="true"
|
||||||
|
convertNoticesToExceptions="true"
|
||||||
|
convertWarningsToExceptions="true"
|
||||||
|
processIsolation="false"
|
||||||
|
stopOnFailure="false"
|
||||||
|
verbose="true">
|
||||||
|
|
||||||
|
<testsuites>
|
||||||
|
<testsuite name="Wearepixel's Laravel QuickBooks Client">
|
||||||
|
<directory>./tests/</directory>
|
||||||
|
</testsuite>
|
||||||
|
</testsuites>
|
||||||
|
|
||||||
|
<php>
|
||||||
|
<!-- <env name="VARIABLE" value="value"/> -->
|
||||||
|
</php>
|
||||||
|
|
||||||
|
<filter>
|
||||||
|
<whitelist processUncoveredFilesFromWhitelist="true">
|
||||||
|
<directory suffix=".php">./src</directory>
|
||||||
|
<exclude>
|
||||||
|
<directory>./src/Http/Exceptions</directory>
|
||||||
|
<directory>./src/Providers</directory>
|
||||||
|
</exclude>
|
||||||
|
</whitelist>
|
||||||
|
</filter>
|
||||||
|
|
||||||
|
<logging>
|
||||||
|
<log type="coverage-html"
|
||||||
|
target="./build/phpunit/coverage"
|
||||||
|
lowUpperBound="35"
|
||||||
|
highLowerBound="70"/>
|
||||||
|
<log type="coverage-text"
|
||||||
|
target="php://stdout"
|
||||||
|
showOnlySummary="true"
|
||||||
|
showUncoveredFiles="false"/>
|
||||||
|
<log type="coverage-clover" target="build/phpunit/logs/clover.xml"/>
|
||||||
|
<log type="junit" target="./build/phpunit/logs/junit.xml"/>
|
||||||
|
</logging>
|
||||||
|
</phpunit>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"preset": "laravel",
|
||||||
|
"rules": {
|
||||||
|
"no_superfluous_phpdoc_tags": true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
|
||||||
|
<title>{{ config('app.name') }} - QuickBooks Authorization</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1 class="page-heading">QuickBooks Authorization</h1>
|
||||||
|
|
||||||
|
{{-- TODO: Text here to explain what is going on --}}
|
||||||
|
{{-- TODO: Work on UI --}}
|
||||||
|
<a href="{!! $authorization_uri !!}">Connect to QuickBooks</a>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
|
||||||
|
<title>{{ config('app.name') }} - QuickBooks Deauthorization</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1 class="page-heading">QuickBooks Deauthorization</h1>
|
||||||
|
|
||||||
|
{{-- TODO: Text here to explain what is going on --}}
|
||||||
|
{{-- TODO: Work on UI --}}
|
||||||
|
<a href="{{ route('quickbooks.disconnect') }}"
|
||||||
|
onclick="event.preventDefault();document.getElementById('disconnect-form').submit();">
|
||||||
|
Disconnect from {{ $company->CompanyName }}
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<form id="disconnect-form"
|
||||||
|
action="{{ route('quickbooks.disconnect') }}"
|
||||||
|
method="POST"
|
||||||
|
style="display: none;"
|
||||||
|
>
|
||||||
|
{{ method_field('DELETE') }}
|
||||||
|
{{ csrf_field() }}
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
+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'],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,299 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Wearepixel\QuickBooks;
|
||||||
|
|
||||||
|
use Mockery;
|
||||||
|
use Mockery\Mock;
|
||||||
|
use QuickBooksOnline\API\DataService\DataService;
|
||||||
|
use QuickBooksOnline\API\Exception\ServiceException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class ClientTest
|
||||||
|
*/
|
||||||
|
class ClientTest extends TestCase
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var Client
|
||||||
|
*/
|
||||||
|
protected $client;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $configs = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Mock
|
||||||
|
*/
|
||||||
|
protected $token_mock;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
$this->configs = [
|
||||||
|
'data_service' => [
|
||||||
|
'auth_mode' => 'oauth2',
|
||||||
|
'base_url' => 'Development',
|
||||||
|
'client_id' => 'QUICKBOOKS_CLIENT_ID',
|
||||||
|
'client_secret' => 'QUICKBOOKS_CLIENT_SECRET',
|
||||||
|
'scope' => 'com.intuit.quickbooks.accounting',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->token_mock = Mockery::mock(Token::class);
|
||||||
|
|
||||||
|
$this->client = $this->makeClient();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function makeClient($configs = null)
|
||||||
|
{
|
||||||
|
if (! is_null($configs)) {
|
||||||
|
return new Client($configs, $this->token_mock);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Client($this->configs, $this->token_mock);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function it_can_be_constructed()
|
||||||
|
{
|
||||||
|
$this->assertInstanceOf(Client::class, $this->client);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function it_returns_a_data_service_configured_to_request_oauth_token_when_token_is_empty()
|
||||||
|
{
|
||||||
|
$this->token_mock
|
||||||
|
->shouldReceive('getAttribute')
|
||||||
|
->twice()
|
||||||
|
->with('hasValidAccessToken')
|
||||||
|
->andReturnFalse();
|
||||||
|
|
||||||
|
$this->token_mock
|
||||||
|
->shouldReceive('getAttribute')
|
||||||
|
->once()
|
||||||
|
->with('hasValidRefreshToken')
|
||||||
|
->andReturnFalse();
|
||||||
|
|
||||||
|
$this->assertFalse(
|
||||||
|
$this->client
|
||||||
|
->getDataService()
|
||||||
|
->getServiceContext()
|
||||||
|
->IppConfiguration->Security->isAccessTokenSet(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function it_caches_the_data_service_once_it_is_made()
|
||||||
|
{
|
||||||
|
$this->token_mock
|
||||||
|
->shouldReceive('getAttribute')
|
||||||
|
->twice()
|
||||||
|
->with('hasValidAccessToken')
|
||||||
|
->andReturnFalse();
|
||||||
|
|
||||||
|
$this->token_mock
|
||||||
|
->shouldReceive('getAttribute')
|
||||||
|
->once()
|
||||||
|
->with('hasValidRefreshToken')
|
||||||
|
->andReturnFalse();
|
||||||
|
|
||||||
|
// TODO: Find out if this is the actual goal of this test, to make sure a DataService is received
|
||||||
|
$this->assertEquals(DataService::class, get_class($this->client->getDataService()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function it_returns_a_data_service_with_oauth_token_when_valid_access_token_exist()
|
||||||
|
{
|
||||||
|
$this->token_mock
|
||||||
|
->shouldReceive('getAttribute')
|
||||||
|
->twice()
|
||||||
|
->with('hasValidAccessToken')
|
||||||
|
->andReturnTrue();
|
||||||
|
|
||||||
|
$this->token_mock
|
||||||
|
->shouldReceive('getAttribute')
|
||||||
|
->once()
|
||||||
|
->with('access_token')
|
||||||
|
->andReturn('access_token');
|
||||||
|
|
||||||
|
$this->token_mock
|
||||||
|
->shouldReceive('getAttribute')
|
||||||
|
->once()
|
||||||
|
->with('realm_id')
|
||||||
|
->andReturn('realm_id');
|
||||||
|
|
||||||
|
$this->token_mock
|
||||||
|
->shouldReceive('getAttribute')
|
||||||
|
->once()
|
||||||
|
->with('refresh_token')
|
||||||
|
->andReturn('refresh_token');
|
||||||
|
|
||||||
|
$this->assertTrue(
|
||||||
|
$this->client
|
||||||
|
->getDataService()
|
||||||
|
->getServiceContext()
|
||||||
|
->IppConfiguration->Security->isAccessTokenSet(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function it_returns_a_data_service_with_refreshed_token_when_access_token_expired_but_refresh_token_valid()
|
||||||
|
{
|
||||||
|
$this->expectException(ServiceException::class);
|
||||||
|
|
||||||
|
$this->token_mock
|
||||||
|
->shouldReceive('getAttribute')
|
||||||
|
->twice()
|
||||||
|
->with('hasValidAccessToken')
|
||||||
|
->andReturnFalse();
|
||||||
|
|
||||||
|
$this->token_mock
|
||||||
|
->shouldReceive('getAttribute')
|
||||||
|
->once()
|
||||||
|
->with('hasValidRefreshToken')
|
||||||
|
->andReturnTrue();
|
||||||
|
|
||||||
|
$this->token_mock
|
||||||
|
->shouldReceive('getAttribute')
|
||||||
|
->never()
|
||||||
|
->with('access_token');
|
||||||
|
|
||||||
|
$this->token_mock
|
||||||
|
->shouldReceive('getAttribute')
|
||||||
|
->once()
|
||||||
|
->with('realm_id')
|
||||||
|
->andReturn('realm_id');
|
||||||
|
|
||||||
|
$this->token_mock
|
||||||
|
->shouldReceive('getAttribute')
|
||||||
|
->once()
|
||||||
|
->with('refresh_token')
|
||||||
|
->andReturn('refresh_token');
|
||||||
|
|
||||||
|
// TODO: This really needs to test that things are getting called correctly on the DataService class
|
||||||
|
$this->assertFalse(
|
||||||
|
$this->client
|
||||||
|
->getDataService()
|
||||||
|
->getServiceContext()
|
||||||
|
->IppConfiguration->Security->isAccessTokenSet(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function it_returns_a_report_service_using_the_data_service()
|
||||||
|
{
|
||||||
|
$this->markTestSkipped(
|
||||||
|
'Once we figure out how to test around the static DataService::Configure',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function it_has_logging_off_by_default()
|
||||||
|
{
|
||||||
|
$this->markTestSkipped('Have to figure out how to test this with the new code in 5.x');
|
||||||
|
|
||||||
|
$this->token_mock
|
||||||
|
->shouldReceive('getAttribute')
|
||||||
|
->once()
|
||||||
|
->with('hasValidAccessToken')
|
||||||
|
->andReturnFalse();
|
||||||
|
|
||||||
|
$this->token_mock
|
||||||
|
->shouldReceive('getAttribute')
|
||||||
|
->once()
|
||||||
|
->with('hasValidRefreshToken')
|
||||||
|
->andReturnFalse();
|
||||||
|
|
||||||
|
$this->assertFalse(
|
||||||
|
filter_var(
|
||||||
|
$this->client->getDataService()->getServiceContext()->IppConfiguration->Logger
|
||||||
|
->RequestLog->EnableRequestResponseLogging,
|
||||||
|
FILTER_VALIDATE_BOOLEAN,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function it_allows_logging_turned_on_and_pointed_to_expected_file()
|
||||||
|
{
|
||||||
|
$this->markTestSkipped('Have to figure out how to test this with the new code in 5.x');
|
||||||
|
|
||||||
|
$this->client = $this->makeClient(
|
||||||
|
array_merge(
|
||||||
|
[
|
||||||
|
'logging' => [
|
||||||
|
'enabled' => true,
|
||||||
|
'location' => '/some/valid/path',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
$this->configs,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->token_mock
|
||||||
|
->shouldReceive('getAttribute')
|
||||||
|
->once()
|
||||||
|
->with('hasValidAccessToken')
|
||||||
|
->andReturnFalse();
|
||||||
|
|
||||||
|
$this->token_mock
|
||||||
|
->shouldReceive('getAttribute')
|
||||||
|
->once()
|
||||||
|
->with('hasValidRefreshToken')
|
||||||
|
->andReturnFalse();
|
||||||
|
|
||||||
|
$this->assertTrue(
|
||||||
|
filter_var(
|
||||||
|
$this->client->getDataService()->getServiceContext()->IppConfiguration->Logger
|
||||||
|
->RequestLog->EnableRequestResponseLogging,
|
||||||
|
FILTER_VALIDATE_BOOLEAN,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertEquals(
|
||||||
|
'/some/valid/path',
|
||||||
|
$this->client->getDataService()->getServiceContext()->IppConfiguration->Logger
|
||||||
|
->RequestLog->ServiceRequestLoggingLocation,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function it_returns_self_after_deleting_token()
|
||||||
|
{
|
||||||
|
$this->token_mock
|
||||||
|
->shouldReceive('remove')
|
||||||
|
->once()
|
||||||
|
->withNoArgs()
|
||||||
|
->andReturnSelf();
|
||||||
|
|
||||||
|
$this->assertInstanceOf(Client::class, $this->client->deleteToken());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function dir($path)
|
||||||
|
{
|
||||||
|
return $path === '/some/valid/path';
|
||||||
|
}
|
||||||
|
|
||||||
|
function route($name)
|
||||||
|
{
|
||||||
|
return $name;
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Wearepixel\QuickBooks;
|
||||||
|
|
||||||
|
use Wearepixel\QuickBooks\Stubs\User;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class HasQuickBooksTokenTest
|
||||||
|
*/
|
||||||
|
class HasQuickBooksTokenTest extends TestCase
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var User
|
||||||
|
*/
|
||||||
|
protected $user;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
|
||||||
|
$this->user = new User();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function it_can_be_constructed()
|
||||||
|
{
|
||||||
|
$this->assertInstanceOf(User::class, $this->user);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function it_has_a_hasOne_relationship_to_token()
|
||||||
|
{
|
||||||
|
$this->assertInstanceOf(Token::class, $this->user->quickBooksToken()->getModel());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,250 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Wearepixel\QuickBooks\Http\Controllers;
|
||||||
|
|
||||||
|
use Illuminate\Contracts\Routing\UrlGenerator;
|
||||||
|
use Illuminate\Contracts\View\Factory as ViewFactory;
|
||||||
|
use Illuminate\Contracts\View\View;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Routing\Redirector;
|
||||||
|
use Illuminate\Session\Store;
|
||||||
|
use Mockery;
|
||||||
|
use Mockery\Mock;
|
||||||
|
use QuickBooksOnline\API\DataService\DataService;
|
||||||
|
use Wearepixel\QuickBooks\Client as QuickBooks;
|
||||||
|
use Wearepixel\QuickBooks\TestCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class ControllerTest
|
||||||
|
*/
|
||||||
|
class ControllerTest extends TestCase
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var Controller
|
||||||
|
*/
|
||||||
|
protected $controller;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Mock
|
||||||
|
*/
|
||||||
|
protected $data_service_mock;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Mock
|
||||||
|
*/
|
||||||
|
protected $quickbooks_mock;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Mock
|
||||||
|
*/
|
||||||
|
protected $redirector_mock;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Mock
|
||||||
|
*/
|
||||||
|
protected $request_mock;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Mock
|
||||||
|
*/
|
||||||
|
protected $session_mock;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Mock
|
||||||
|
*/
|
||||||
|
protected $view_factory_mock;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Mock
|
||||||
|
*/
|
||||||
|
protected $view_mock;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
$this->data_service_mock = Mockery::mock(DataService::class);
|
||||||
|
$this->quickbooks_mock = Mockery::mock(QuickBooks::class);
|
||||||
|
$this->redirector_mock = Mockery::mock(Redirector::class);
|
||||||
|
$this->request_mock = Mockery::mock(Request::class);
|
||||||
|
$this->session_mock = Mockery::mock(Store::class);
|
||||||
|
$this->view_factory_mock = Mockery::mock(ViewFactory::class);
|
||||||
|
$this->view_mock = Mockery::mock(View::class);
|
||||||
|
|
||||||
|
$this->controller = new Controller();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function it_can_be_constructed()
|
||||||
|
{
|
||||||
|
$this->assertInstanceOf(Controller::class, $this->controller);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function it_shows_view_to_connect_if_account_not_linked()
|
||||||
|
{
|
||||||
|
$this->data_service_mock
|
||||||
|
->shouldReceive('getCompanyInfo')
|
||||||
|
->once()
|
||||||
|
->withNoArgs()
|
||||||
|
->andReturn([
|
||||||
|
'name' => 'Company',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->quickbooks_mock
|
||||||
|
->shouldReceive('hasValidRefreshToken')
|
||||||
|
->once()
|
||||||
|
->withNoArgs()
|
||||||
|
->andReturnTrue();
|
||||||
|
|
||||||
|
$this->quickbooks_mock
|
||||||
|
->shouldReceive('getDataService')
|
||||||
|
->once()
|
||||||
|
->withNoArgs()
|
||||||
|
->andReturn($this->data_service_mock);
|
||||||
|
|
||||||
|
$this->view_factory_mock
|
||||||
|
->shouldReceive('make')
|
||||||
|
->once()
|
||||||
|
->with('quickbooks::disconnect')
|
||||||
|
->andReturn($this->view_mock);
|
||||||
|
|
||||||
|
$this->view_mock
|
||||||
|
->shouldReceive('with')
|
||||||
|
->once()
|
||||||
|
->withArgs([
|
||||||
|
'company',
|
||||||
|
[
|
||||||
|
'name' => 'Company',
|
||||||
|
],
|
||||||
|
])
|
||||||
|
->andReturnSelf();
|
||||||
|
|
||||||
|
$this->controller->connect($this->quickbooks_mock, $this->view_factory_mock);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function it_shows_view_to_disconnect_if_account_linked()
|
||||||
|
{
|
||||||
|
$this->quickbooks_mock
|
||||||
|
->shouldReceive('hasValidRefreshToken')
|
||||||
|
->once()
|
||||||
|
->withNoArgs()
|
||||||
|
->andReturnFalse();
|
||||||
|
|
||||||
|
$this->quickbooks_mock
|
||||||
|
->shouldReceive('authorizationUri')
|
||||||
|
->once()
|
||||||
|
->withNoArgs()
|
||||||
|
->andReturn('http://uri');
|
||||||
|
|
||||||
|
$this->view_factory_mock
|
||||||
|
->shouldReceive('make')
|
||||||
|
->once()
|
||||||
|
->with('quickbooks::connect')
|
||||||
|
->andReturn($this->view_mock);
|
||||||
|
|
||||||
|
$this->view_mock
|
||||||
|
->shouldReceive('with')
|
||||||
|
->once()
|
||||||
|
->withArgs(['authorization_uri', 'http://uri'])
|
||||||
|
->andReturnSelf();
|
||||||
|
|
||||||
|
$this->controller->connect($this->quickbooks_mock, $this->view_factory_mock);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function it_disconnects_from_quickbooks_when_requested()
|
||||||
|
{
|
||||||
|
$this->request_mock
|
||||||
|
->shouldReceive('session')
|
||||||
|
->once()
|
||||||
|
->andReturn($this->session_mock);
|
||||||
|
|
||||||
|
$this->session_mock
|
||||||
|
->shouldReceive('flash')
|
||||||
|
->once()
|
||||||
|
->withAnyArgs();
|
||||||
|
|
||||||
|
$this->redirector_mock
|
||||||
|
->shouldReceive('back')
|
||||||
|
->once()
|
||||||
|
->andReturn(new RedirectResponse('/test', 302));
|
||||||
|
|
||||||
|
$this->quickbooks_mock
|
||||||
|
->shouldReceive('deleteToken')
|
||||||
|
->once()
|
||||||
|
->withNoArgs();
|
||||||
|
|
||||||
|
$result = $this->controller->disconnect(
|
||||||
|
$this->redirector_mock,
|
||||||
|
$this->request_mock,
|
||||||
|
$this->quickbooks_mock,
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertInstanceOf(RedirectResponse::class, $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function it_finishes_connecting_to_quickbooks_when_given_a_valid_token_by_quickbooks()
|
||||||
|
{
|
||||||
|
$this->url_generator_mock = Mockery::mock(UrlGenerator::class);
|
||||||
|
$realmId = random_int(1, 9999);
|
||||||
|
|
||||||
|
$this->quickbooks_mock
|
||||||
|
->shouldReceive('exchangeCodeForToken')
|
||||||
|
->once()
|
||||||
|
->withArgs(['code', $realmId]);
|
||||||
|
|
||||||
|
$this->request_mock
|
||||||
|
->shouldReceive('get')
|
||||||
|
->once()
|
||||||
|
->withArgs(['code'])
|
||||||
|
->andReturn('code');
|
||||||
|
|
||||||
|
$this->request_mock
|
||||||
|
->shouldReceive('get')
|
||||||
|
->once()
|
||||||
|
->withArgs(['realmId'])
|
||||||
|
->andReturn($realmId);
|
||||||
|
|
||||||
|
$this->request_mock
|
||||||
|
->shouldReceive('session')
|
||||||
|
->once()
|
||||||
|
->andReturn($this->session_mock);
|
||||||
|
|
||||||
|
$this->session_mock
|
||||||
|
->shouldReceive('flash')
|
||||||
|
->once()
|
||||||
|
->withAnyArgs();
|
||||||
|
|
||||||
|
$this->redirector_mock
|
||||||
|
->shouldReceive('intended')
|
||||||
|
->once()
|
||||||
|
->withAnyArgs()
|
||||||
|
->andReturn(new RedirectResponse('/test', 302));
|
||||||
|
|
||||||
|
$this->url_generator_mock
|
||||||
|
->shouldReceive('route')
|
||||||
|
->withArgs(['quickbooks.connect'])
|
||||||
|
->once();
|
||||||
|
|
||||||
|
$result = $this->controller->token(
|
||||||
|
$this->redirector_mock,
|
||||||
|
$this->request_mock,
|
||||||
|
$this->quickbooks_mock,
|
||||||
|
$this->url_generator_mock,
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertInstanceOf(RedirectResponse::class, $result);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Wearepixel\QuickBooks\Http\Middleware;
|
||||||
|
|
||||||
|
use Illuminate\Contracts\Routing\UrlGenerator;
|
||||||
|
use Illuminate\Contracts\Session\Session;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Routing\Redirector;
|
||||||
|
use Mockery;
|
||||||
|
use Mockery\Mock;
|
||||||
|
use Wearepixel\QuickBooks\Client as QuickBooks;
|
||||||
|
use Wearepixel\QuickBooks\TestCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class FilterTest
|
||||||
|
*/
|
||||||
|
class FilterTest extends TestCase
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var Filter
|
||||||
|
*/
|
||||||
|
protected $filter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Mock
|
||||||
|
*/
|
||||||
|
protected $quickbooks_mock;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Mock
|
||||||
|
*/
|
||||||
|
protected $redirector_mock;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Mock
|
||||||
|
*/
|
||||||
|
protected $request_mock;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Mock
|
||||||
|
*/
|
||||||
|
protected $session_mock;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Mock
|
||||||
|
*/
|
||||||
|
protected $url_generator_mock;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
$this->quickbooks_mock = Mockery::mock(QuickBooks::class);
|
||||||
|
$this->redirector_mock = Mockery::mock(Redirector::class);
|
||||||
|
$this->request_mock = Mockery::mock(Request::class);
|
||||||
|
$this->session_mock = Mockery::mock(Session::class);
|
||||||
|
$this->url_generator_mock = Mockery::mock(UrlGenerator::class);
|
||||||
|
|
||||||
|
$this->filter = new Filter(
|
||||||
|
$this->quickbooks_mock,
|
||||||
|
$this->redirector_mock,
|
||||||
|
$this->session_mock,
|
||||||
|
$this->url_generator_mock,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function it_can_be_constructed()
|
||||||
|
{
|
||||||
|
$this->assertInstanceOf(Filter::class, $this->filter);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function it_just_passes_the_request_to_the_next_middleware_if_account_linked_to_quickbooks()
|
||||||
|
{
|
||||||
|
$next_middleware = function ($request) {
|
||||||
|
$this->assertEquals($this->request_mock, $request);
|
||||||
|
};
|
||||||
|
|
||||||
|
$this->quickbooks_mock
|
||||||
|
->shouldReceive('hasValidRefreshToken')
|
||||||
|
->once()
|
||||||
|
->withNoArgs()
|
||||||
|
->andReturnTrue();
|
||||||
|
|
||||||
|
$this->filter->handle($this->request_mock, $next_middleware);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function it_redirects_to_quickbooks_connect_route_after_setting_intended_session_if_account_not_linked()
|
||||||
|
{
|
||||||
|
$this->request_mock
|
||||||
|
->shouldReceive('path')
|
||||||
|
->once()
|
||||||
|
->withNoArgs()
|
||||||
|
->andReturn('path');
|
||||||
|
|
||||||
|
$this->url_generator_mock
|
||||||
|
->shouldReceive('to')
|
||||||
|
->once()
|
||||||
|
->with('path')
|
||||||
|
->andReturn('http://to/path');
|
||||||
|
|
||||||
|
$this->session_mock
|
||||||
|
->shouldReceive('put')
|
||||||
|
->once()
|
||||||
|
->withArgs(['url.intended', 'http://to/path'])
|
||||||
|
->andReturnNull();
|
||||||
|
|
||||||
|
$this->redirector_mock
|
||||||
|
->shouldReceive('route')
|
||||||
|
->once()
|
||||||
|
->with('quickbooks.connect')
|
||||||
|
->andReturnSelf();
|
||||||
|
|
||||||
|
$next_middleware = function ($request) {
|
||||||
|
// If this is called, then fail test
|
||||||
|
$this->assertTrue(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
$this->quickbooks_mock
|
||||||
|
->shouldReceive('hasValidRefreshToken')
|
||||||
|
->once()
|
||||||
|
->withNoArgs()
|
||||||
|
->andReturnFalse();
|
||||||
|
|
||||||
|
$this->filter->handle($this->request_mock, $next_middleware);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Wearepixel\QuickBooks\Stubs;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
use Mockery;
|
||||||
|
use Wearepixel\QuickBooks\Token;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class TokenStub
|
||||||
|
*
|
||||||
|
* Stub for a Laravel Token model
|
||||||
|
*/
|
||||||
|
class TokenStub extends Token
|
||||||
|
{
|
||||||
|
// Put public properties on this stub to keep the Model from trying to get them from db
|
||||||
|
public $access_token = null;
|
||||||
|
|
||||||
|
public $access_token_expires_at = null;
|
||||||
|
|
||||||
|
public $realm_id = null;
|
||||||
|
|
||||||
|
public $refresh_token = null;
|
||||||
|
|
||||||
|
public $refresh_token_expires_at = null;
|
||||||
|
|
||||||
|
public $user = null;
|
||||||
|
|
||||||
|
public function belongsTo(
|
||||||
|
$related,
|
||||||
|
$foreignKey = null,
|
||||||
|
$ownerKey = null,
|
||||||
|
$relation = null,
|
||||||
|
): BelongsTo {
|
||||||
|
$related_mock = Mockery::mock($related);
|
||||||
|
$related_mock->shouldIgnoreMissing();
|
||||||
|
|
||||||
|
$child_mock = Mockery::mock(Model::class);
|
||||||
|
$child_mock->shouldIgnoreMissing();
|
||||||
|
|
||||||
|
$builder_mock = Mockery::mock(Builder::class);
|
||||||
|
$builder_mock->shouldIgnoreMissing();
|
||||||
|
$builder_mock->shouldReceive('getModel')->andReturn($related_mock);
|
||||||
|
|
||||||
|
return new BelongsTo($builder_mock, $child_mock, $foreignKey, $ownerKey, $relation);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Wearepixel\QuickBooks\Stubs;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||||
|
use Mockery;
|
||||||
|
use Wearepixel\QuickBooks\HasQuickBooksToken;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class User
|
||||||
|
*
|
||||||
|
* Stub for a Laravel User model
|
||||||
|
*/
|
||||||
|
class User
|
||||||
|
{
|
||||||
|
use HasQuickBooksToken;
|
||||||
|
|
||||||
|
public function hasOne($relationship): HasOne
|
||||||
|
{
|
||||||
|
$related_mock = Mockery::mock($relationship);
|
||||||
|
$related_mock->shouldIgnoreMissing();
|
||||||
|
|
||||||
|
$builder_mock = Mockery::mock(Builder::class);
|
||||||
|
$builder_mock->shouldIgnoreMissing();
|
||||||
|
$builder_mock->shouldReceive('getModel')->andReturn($related_mock);
|
||||||
|
|
||||||
|
$parent_mock = Mockery::mock(Model::class);
|
||||||
|
$parent_mock->shouldIgnoreMissing();
|
||||||
|
|
||||||
|
return new HasOne($builder_mock, $parent_mock, 'foreignKey', 'localKey');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Wearepixel\QuickBooks;
|
||||||
|
|
||||||
|
use ArrayAccess;
|
||||||
|
use Countable;
|
||||||
|
use Iterator;
|
||||||
|
use Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration;
|
||||||
|
use Mockery\MockInterface;
|
||||||
|
use PHPUnit\Framework\TestCase as PHPUnitTestCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class TestCase
|
||||||
|
*/
|
||||||
|
abstract class TestCase extends PHPUnitTestCase
|
||||||
|
{
|
||||||
|
use MockeryPHPUnitIntegration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to allow mocking of iterator classes.
|
||||||
|
*
|
||||||
|
* @link https://gist.github.com/VladaHejda/8299871
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
protected function mockArrayIterator(MockInterface $mock, array $items)
|
||||||
|
{
|
||||||
|
if ($mock instanceof ArrayAccess) {
|
||||||
|
foreach ($items as $key => $val) {
|
||||||
|
$mock
|
||||||
|
->shouldReceive('offsetGet')
|
||||||
|
->with($key)
|
||||||
|
->andReturn($val);
|
||||||
|
|
||||||
|
$mock
|
||||||
|
->shouldReceive('offsetExists')
|
||||||
|
->with($key)
|
||||||
|
->andReturn(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
$mock->shouldReceive('offsetExists')->andReturn(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($mock instanceof Iterator) {
|
||||||
|
$counter = 0;
|
||||||
|
|
||||||
|
$mock->shouldReceive('rewind')->andReturnUsing(function () use (&$counter) {
|
||||||
|
$counter = 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
$vals = array_values($items);
|
||||||
|
$keys = array_values(array_keys($items));
|
||||||
|
|
||||||
|
$mock->shouldReceive('valid')->andReturnUsing(function () use (&$counter, $vals) {
|
||||||
|
return isset($vals[$counter]);
|
||||||
|
});
|
||||||
|
|
||||||
|
$mock->shouldReceive('current')->andReturnUsing(function () use (&$counter, $vals) {
|
||||||
|
return $vals[$counter];
|
||||||
|
});
|
||||||
|
|
||||||
|
$mock->shouldReceive('key')->andReturnUsing(function () use (&$counter, $keys) {
|
||||||
|
return $keys[$counter];
|
||||||
|
});
|
||||||
|
|
||||||
|
$mock->shouldReceive('next')->andReturnUsing(function () use (&$counter) {
|
||||||
|
$counter++;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($mock instanceof Countable) {
|
||||||
|
$mock->shouldReceive('count')->andReturn(count($items));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,212 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Wearepixel\QuickBooks;
|
||||||
|
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||||
|
use Mockery;
|
||||||
|
use QuickBooksOnline\API\Core\OAuth\OAuth2\OAuth2AccessToken;
|
||||||
|
use Wearepixel\QuickBooks\Stubs\TokenStub;
|
||||||
|
use Wearepixel\QuickBooks\Stubs\User;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class TokenTest
|
||||||
|
*/
|
||||||
|
class TokenTest extends TestCase
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var Token
|
||||||
|
*/
|
||||||
|
protected $token;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
$this->token = new TokenStub();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function it_can_be_constructed()
|
||||||
|
{
|
||||||
|
$this->assertInstanceOf(Token::class, $this->token);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function it_has_accessor_for_valid_access_token()
|
||||||
|
{
|
||||||
|
$this->assertNotNull($this->token->getHasValidAccessTokenAttribute());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function it_has_accessor_for_valid_refresh_token()
|
||||||
|
{
|
||||||
|
$this->assertNotNull($this->token->getHasValidRefreshTokenAttribute());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function it_knows_that_the_access_token_expires_at_has_to_be_valid()
|
||||||
|
{
|
||||||
|
$this->assertFalse($this->token->getHasValidAccessTokenAttribute());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function it_knows_that_the_refresh_token_expires_at_has_to_be_valid()
|
||||||
|
{
|
||||||
|
$this->assertFalse($this->token->getHasValidRefreshTokenAttribute());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function it_know_if_access_token_expires_at_is_less_than_now_it_is_not_expired()
|
||||||
|
{
|
||||||
|
$now = Carbon::now();
|
||||||
|
Carbon::setTestNow($now);
|
||||||
|
|
||||||
|
$this->token->access_token_expires_at = Carbon::now()->addSecond();
|
||||||
|
|
||||||
|
$this->assertTrue($this->token->getHasValidAccessTokenAttribute());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function it_know_if_refresh_token_expires_at_is_less_than_now_it_is_not_expired()
|
||||||
|
{
|
||||||
|
$now = Carbon::now();
|
||||||
|
Carbon::setTestNow($now);
|
||||||
|
|
||||||
|
$this->token->refresh_token_expires_at = Carbon::now()->addSecond();
|
||||||
|
|
||||||
|
$this->assertTrue($this->token->getHasValidRefreshTokenAttribute());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function it_know_if_access_token_expires_at_is_greater_than_or_equal_to_now_it_is_expired()
|
||||||
|
{
|
||||||
|
$now = Carbon::now();
|
||||||
|
Carbon::setTestNow($now);
|
||||||
|
|
||||||
|
$this->token->access_token_expires_at = Carbon::now();
|
||||||
|
|
||||||
|
$this->assertFalse($this->token->getHasValidAccessTokenAttribute());
|
||||||
|
|
||||||
|
$this->token->access_token_expires_at->subSecond();
|
||||||
|
|
||||||
|
$this->assertFalse($this->token->getHasValidAccessTokenAttribute());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function it_know_if_refresh_token_expires_at_is_greater_than_or_equal_to_now_it_is_expired()
|
||||||
|
{
|
||||||
|
$now = Carbon::now();
|
||||||
|
Carbon::setTestNow($now);
|
||||||
|
|
||||||
|
$this->token->refresh_token_expires_at = Carbon::now();
|
||||||
|
|
||||||
|
$this->assertFalse($this->token->getHasValidRefreshTokenAttribute());
|
||||||
|
|
||||||
|
$this->token->refresh_token_expires_at->subSecond();
|
||||||
|
|
||||||
|
$this->assertFalse($this->token->getHasValidRefreshTokenAttribute());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function it_stores_the_oauth_token_parts_in_expected_properties()
|
||||||
|
{
|
||||||
|
$oauth_token_mock = Mockery::mock(OAuth2AccessToken::class);
|
||||||
|
|
||||||
|
$oauth_token_mock
|
||||||
|
->shouldReceive('getAccessToken')
|
||||||
|
->once()
|
||||||
|
->withNoArgs()
|
||||||
|
->andReturn('access_token');
|
||||||
|
|
||||||
|
$oauth_token_mock
|
||||||
|
->shouldReceive('getAccessTokenExpiresAt')
|
||||||
|
->once()
|
||||||
|
->withNoArgs()
|
||||||
|
->andReturn('now');
|
||||||
|
|
||||||
|
$oauth_token_mock
|
||||||
|
->shouldReceive('getRealmID')
|
||||||
|
->once()
|
||||||
|
->withNoArgs()
|
||||||
|
->andReturn('realm_id');
|
||||||
|
|
||||||
|
$oauth_token_mock
|
||||||
|
->shouldReceive('getRefreshToken')
|
||||||
|
->once()
|
||||||
|
->withNoArgs()
|
||||||
|
->andReturn('refresh_token');
|
||||||
|
|
||||||
|
$oauth_token_mock
|
||||||
|
->shouldReceive('getRefreshTokenExpiresAt')
|
||||||
|
->once()
|
||||||
|
->withNoArgs()
|
||||||
|
->andReturn('now');
|
||||||
|
|
||||||
|
$this->token->parseOauthToken($oauth_token_mock);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function it_allows_itself_to_be_deleted_and_returns_new_token()
|
||||||
|
{
|
||||||
|
$this->token->user = Mockery::mock(User::class);
|
||||||
|
|
||||||
|
$has_one_mock = Mockery::mock(HasOne::class);
|
||||||
|
$token_mock = Mockery::mock(Token::class);
|
||||||
|
|
||||||
|
$has_one_mock
|
||||||
|
->shouldReceive('make')
|
||||||
|
->once()
|
||||||
|
->withNoArgs()
|
||||||
|
->andReturn($token_mock);
|
||||||
|
|
||||||
|
$this->token->user
|
||||||
|
->shouldReceive('quickBooksToken')
|
||||||
|
->once()
|
||||||
|
->withNoArgs()
|
||||||
|
->andReturn($has_one_mock);
|
||||||
|
|
||||||
|
$this->token->user->id = 1;
|
||||||
|
|
||||||
|
$this->assertEquals($token_mock, $this->token->remove());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function it_get_related_user_model_from_configuration()
|
||||||
|
{
|
||||||
|
$this->assertInstanceOf(User::class, $this->token->user()->getModel());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function config($key)
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'keys' => [
|
||||||
|
'foreign' => 'user_id',
|
||||||
|
'owner' => 'id',
|
||||||
|
],
|
||||||
|
'model' => User::class,
|
||||||
|
];
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user