1
0
mirror of https://github.com/invoiceninja/invoiceninja.git synced 2024-11-09 20:52:56 +01:00

Updates for QB

This commit is contained in:
David Bomba 2024-08-23 14:17:37 +10:00
parent ac742cc893
commit b24d843164
13 changed files with 199 additions and 454 deletions

View File

@ -3,28 +3,19 @@
namespace App\Http\Controllers;
use App\Http\Requests\Quickbooks\AuthorizedQuickbooksRequest;
use Closure;
use App\Utils\Ninja;
use App\Models\Company;
use App\Libraries\MultiDB;
use Illuminate\Support\Str;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use App\Utils\Traits\MakesHash;
use App\Jobs\Import\QuickbooksIngest;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Validator;
use App\Http\Requests\Quickbooks\AuthQuickbooksRequest;
use App\Services\Import\Quickbooks\QuickbooksService;
class ImportQuickbooksController extends BaseController
{
private array $import_entities = [
'client' => 'Customer',
'invoice' => 'Invoice',
'product' => 'Item',
'payment' => 'Payment'
];
// private array $import_entities = [
// 'client' => 'Customer',
// 'invoice' => 'Invoice',
// 'product' => 'Item',
// 'payment' => 'Payment'
// ];
public function onAuthorized(AuthorizedQuickbooksRequest $request)
{
@ -34,12 +25,11 @@ class ImportQuickbooksController extends BaseController
$qb = new QuickbooksService($company);
$realm = $request->query('realmId');
$access_token_object = $qb->getAuth()->accessToken($request->query('code'), $realm);
nlog($access_token_object); //OAuth2AccessToken
$company->quickbooks = $access_token_object;
$company->save();
$access_token_object = $qb->getAuth()->accessTokenFromCode($request->query('code'), $realm);
$qb->getAuth()->saveOAuthToken($access_token_object);
return redirect(config('ninja.react_url'));
return response()->json(['message' => 'Success'], 200); //todo swapout for redirect to UI
}
/**
@ -63,29 +53,29 @@ class ImportQuickbooksController extends BaseController
public function preimport(string $type, string $hash)
{
// Check for authorization otherwise
// Create a reference
$data = [
'hash' => $hash,
'type' => $type
];
$this->getData($data);
// // Check for authorization otherwise
// // Create a reference
// $data = [
// 'hash' => $hash,
// 'type' => $type
// ];
// $this->getData($data);
}
protected function getData($data)
{
$entity = $this->import_entities[$data['type']];
$cache_name = "{$data['hash']}-{$data['type']}";
// TODO: Get or put cache or DB?
if(! Cache::has($cache_name)) {
$contents = call_user_func([$this->service, "fetch{$entity}s"]);
if($contents->isEmpty()) {
return;
}
// $entity = $this->import_entities[$data['type']];
// $cache_name = "{$data['hash']}-{$data['type']}";
// // TODO: Get or put cache or DB?
// if(! Cache::has($cache_name)) {
// $contents = call_user_func([$this->service, "fetch{$entity}s"]);
// if($contents->isEmpty()) {
// return;
// }
Cache::put($cache_name, base64_encode($contents->toJson()), 600);
}
// Cache::put($cache_name, base64_encode($contents->toJson()), 600);
// }
}
/**

View File

@ -1,11 +0,0 @@
<?php
namespace App\Repositories\Import\Quickbooks;
use App\Repositories\Import\Quickbooks\Repository;
use App\Repositories\Import\Quickbooks\Contracts\RepositoryInterface;
class CustomerRepository extends Repository implements RepositoryInterface
{
protected string $entity = "Customer";
}

View File

@ -1,10 +0,0 @@
<?php
namespace App\Repositories\Import\Quickbooks;
use App\Repositories\Import\Quickbooks\Contracts\RepositoryInterface as QuickbooksInterface;
class InvoiceRepository extends Repository implements QuickbooksInterface
{
protected string $entity = "Invoice";
}

View File

@ -1,11 +0,0 @@
<?php
namespace App\Repositories\Import\Quickbooks;
use App\Repositories\Import\Quickbooks\Repository;
use App\Repositories\Import\Quickbooks\Contracts\RepositoryInterface;
class ItemRepository extends Repository implements RepositoryInterface
{
protected string $entity = "Item";
}

View File

@ -1,10 +0,0 @@
<?php
namespace App\Repositories\Import\Quickbooks;
use App\Repositories\Import\Quickbooks\Contracts\RepositoryInterface as QuickbooksInterface;
class PaymentRepository extends Repository implements QuickbooksInterface
{
protected string $entity = "Payment";
}

View File

@ -1,38 +0,0 @@
<?php
namespace App\Repositories\Import\Quickbooks;
use Illuminate\Support\Collection;
use App\Repositories\Import\Quickbooks\Contracts\RepositoryInterface;
use App\Services\Import\Quickbooks\Contracts\SdkInterface as QuickbooksInterface;
use App\Repositories\Import\Quickbooks\Transformers\Transformer as QuickbooksTransformer;
abstract class Repository implements RepositoryInterface
{
protected string $entity;
protected QuickbooksInterface $db;
protected QuickbooksTransformer $transfomer;
public function __construct(QuickbooksInterface $db, QuickbooksTransformer $transfomer)
{
$this->db = $db;
$this->transformer = $transfomer;
}
public function count(): int
{
return $this->db->totalRecords($this->entity);
}
public function all(): Collection
{
return $this->get($this->count());
}
public function get(int $max = 100): Collection
{
return $this->transformer->transform($this->db->fetchRecords($this->entity, $max), $this->entity);
}
}

View File

@ -1,64 +0,0 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Services\Import\Quickbooks;
final class Auth
{
public function __construct(private SdkWrapper $sdk)
{
}
public function accessToken(string $code, string $realm): array
{
// TODO: Get or put token in Cache or DB?
return $this->sdk->accessToken($code, $realm);
}
public function refreshToken(): array
{
// TODO: Get or put token in Cache or DB?
return $this->sdk->refreshToken();
}
public function getAuthorizationUrl(): string
{
return $this->sdk->getAuthorizationUrl();
}
public function getState(): string
{
return $this->sdk->getState();
}
public function getAccessToken(): array
{
$tokens = [];
// $token_store = new CompanyTokensRepository();
// $tokens = $token_store->get();
// if(empty($tokens)) {
// $token = $this->sdk->getAccessToken();
// $access_token = $token->getAccessToken();
// $realm = $token->getRealmID();
// $refresh_token = $token->getRefreshToken();
// $access_token_expires = $token->getAccessTokenExpiresAt();
// $refresh_token_expires = $token->getRefreshTokenExpiresAt();
// $tokens = compact('access_token', 'refresh_token','access_token_expires', 'refresh_token_expires','realm');
// }
return $tokens;
}
public function getRefreshToken(): array
{
return $this->getAccessToken();
}
}

View File

@ -1,14 +0,0 @@
<?php
namespace App\Services\Import\Quickbooks\Contracts;
interface SdkInterface
{
public function getAuthorizationUrl(): string;
public function accessToken(string $code, string $realm): array;
public function refreshToken(): array;
public function getAccessToken();
public function getRefreshToken(): array;
public function totalRecords(string $entity): int;
public function fetchRecords(string $entity, int $max): array;
}

View File

@ -11,40 +11,45 @@
namespace App\Services\Import\Quickbooks;
use Carbon\Carbon;
use App\Models\Company;
use QuickBooksOnline\API\Core\CoreConstants;
use QuickBooksOnline\API\DataService\DataService;
use QuickBooksOnline\API\Core\OAuth\OAuth2\OAuth2AccessToken;
// quickbooks_realm_id
// quickbooks_refresh_token
// quickbooks_refresh_expires
class QuickbooksService
{
private DataService $sdk;
public DataService $sdk;
private Auth $auth;
private bool $testMode = true;
public function __construct(private Company $company)
{
$this->init()
->auth();
$this->init();
}
private function init(): self
{
$this->sdk = DataService::Configure([
$config = [
'ClientID' => config('services.quickbooks.client_id'),
'ClientSecret' => config('services.quickbooks.client_secret'),
'auth_mode' => 'oauth2',
'scope' => "com.intuit.quickbooks.accounting",
'RedirectURI' => 'https://developer.intuit.com/v2/OAuth2Playground/RedirectUrl',
// 'RedirectURI' => route('quickbooks.authorized'),
]);
// 'RedirectURI' => 'https://developer.intuit.com/v2/OAuth2Playground/RedirectUrl',
'RedirectURI' => 'https://above-distinctly-teal.ngrok-free.app/quickbooks/authorized',
'baseUrl' => $this->testMode ? CoreConstants::SANDBOX_DEVELOPMENT : CoreConstants::QBO_BASEURL,
];
// if (env('APP_DEBUG')) {
// $sdk->setLogLocation(storage_path("logs/quickbooks.log"));
// $sdk->enableLog();
// }
$merged = array_merge($config, $this->ninjaAccessToken());
nlog($merged);
$this->sdk = DataService::Configure($merged);
$this->sdk->setLogLocation(storage_path("logs/quickbooks.log"));
$this->sdk->enableLog();
$this->sdk->setMinorVersion("73");
$this->sdk->throwExceptionOnError(true);
@ -52,12 +57,13 @@ class QuickbooksService
return $this;
}
private function auth(): self
private function ninjaAccessToken()
{
$wrapper = new SdkWrapper($this->sdk);
$this->auth = new Auth($wrapper);
return $this;
return isset($this->company->quickbooks->accessTokenKey) ? [
'accessTokenKey' => $this->company->quickbooks->accessTokenKey,
'refreshTokenKey' => $this->company->quickbooks->refresh_token,
'QBORealmID' => $this->company->quickbooks->realmID,
] : [];
}
public function getSdk(): DataService
@ -65,8 +71,9 @@ class QuickbooksService
return $this->sdk;
}
public function getAuth(): Auth
public function getAuth(): SdkWrapper
{
return $this->auth;
return new SdkWrapper($this->sdk, $this->company);
}
}

View File

@ -1,78 +0,0 @@
<?php
namespace App\Services\Import\Quickbooks\Repositories;
use App\Models\Company;
use App\Libraries\MultiDB;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Cache;
class CompanyTokensRepository
{
private $company_key;
private $store_key = "quickbooks-token";
public function __construct(string $key = null)
{
$this->company_key = $key ?? auth()->user->company()->company_key ?? null;
$this->store_key .= $key;
$this->setCompanyDbByKey();
}
public function save(array $tokens)
{
$this->updateAccessToken($tokens['access_token'], $tokens['access_token_expires']);
$this->updateRefreshToken($tokens['refresh_token'], $tokens['refresh_token_expires'], $tokens['realm']);
}
public function findByCompanyKey(): ?Company
{
return Company::where('company_key', $this->company_key)->first();
}
public function setCompanyDbByKey()
{
MultiDB::findAndSetDbByCompanyKey($this->company_key);
}
public function get()
{
return $this->getAccessToken() + $this->getRefreshToken();
}
protected function updateRefreshToken(string $token, string $expires, string $realm)
{
DB::table('companies')
->where('company_key', $this->company_key)
->update(['quickbooks_refresh_token' => $token,
'quickbooks_realm_id' => $realm,
'quickbooks_refresh_expires' => $expires ]);
}
protected function updateAccessToken(string $token, string $expires)
{
Cache::put([$this->store_key => $token], $expires);
}
protected function getAccessToken()
{
$result = Cache::get($this->store_key);
return $result ? ['access_token' => $result] : [];
}
protected function getRefreshToken()
{
$result = (array) DB::table('companies')
->select('quickbooks_refresh_token', 'quickbooks_realm_id')
->where('company_key', $this->company_key)
->where('quickbooks_refresh_expires', '>', now())
->first();
return $result ? array_combine(['refresh_token','realm'], array_values($result)) : [];
}
}

View File

@ -2,77 +2,151 @@
namespace App\Services\Import\Quickbooks;
use App\Services\Import\Quickbooks\Contracts\SdkInterface as QuickbooksInterface;
use Carbon\Carbon;
use App\Models\Company;
use QuickBooksOnline\API\DataService\DataService;
use QuickBooksOnline\API\Core\OAuth\OAuth2\OAuth2AccessToken;
final class SdkWrapper implements QuickbooksInterface
class SdkWrapper
{
public const MAXRESULTS = 10000;
private $sdk;
private $entities = ['Customer','Invoice','Payment','Item'];
public function __construct($sdk)
private OAuth2AccessToken $token;
public function __construct(public DataService $sdk, private Company $company)
{
// Prep Data Services
$this->sdk = $sdk;
$this->init();
}
private function init(): self
{
isset($this->company->quickbooks->accessTokenKey) ? $this->setNinjaAccessToken($this->company->quickbooks) : null;
return $this;
}
public function getAuthorizationUrl(): string
{
return ($this->sdk->getOAuth2LoginHelper())->getAuthorizationCodeURL();
return $this->sdk->getOAuth2LoginHelper()->getAuthorizationCodeURL();
}
public function getState(): string
{
return ($this->sdk->getOAuth2LoginHelper())->getState();
return $this->sdk->getOAuth2LoginHelper()->getState();
}
public function getAccessToken()
public function getRefreshToken(): string
{
return $this->getTokens();
return $this->accessToken()->getRefreshToken();
}
public function company()
{
nlog("getting company info");
// nlog($this->sdk->getAccessToken());
return $this->sdk->getCompanyInfo();
}
/*
accessTokenKey
tokenType
refresh_token
accessTokenExpiresAt
refreshTokenExpiresAt
accessTokenValidationPeriod
refreshTokenValidationPeriod
clientID
clientSecret
realmID
baseURL
*/
public function accessTokenFromCode(string $code, string $realm): OAuth2AccessToken
{
$token = $this->sdk->getOAuth2LoginHelper()->exchangeAuthorizationCodeForToken($code, $realm);
$this->setAccessToken($token);
return $this->accessToken();
}
/**
* Set Stored NinjaAccessToken
*
* @param mixed $token_object
* @return self
*/
public function setNinjaAccessToken(mixed $token_object): self
{
$token = new OAuth2AccessToken(
config('services.quickbooks.client_id'),
config('services.quickbooks.client_secret'),
$token_object->accessTokenKey,
$token_object->refresh_token,
3600,
8726400
);
$token->setAccessTokenExpiresAt($token_object->accessTokenExpiresAt);
$token->setRefreshTokenExpiresAt($token_object->refreshTokenExpiresAt);
$token->setAccessTokenValidationPeriodInSeconds(3600);
$token->setRefreshTokenValidationPeriodInSeconds(8726400);
$this->setAccessToken($token);
if($token_object->accessTokenExpiresAt < time()){
$new_token = $this->sdk->getOAuth2LoginHelper()->refreshToken();
nlog("getting new token");
$this->setAccessToken($new_token);
$this->saveOAuthToken($this->accessToken());
}
return $this;
}
/**
* SetsAccessToken
*
* @param OAuth2AccessToken $token
* @return self
*/
public function setAccessToken(OAuth2AccessToken $token): self
{
// $this->sdk = $this->sdk->updateOAuth2Token($token);
$this->token = $token;
nlog("set access token");
nlog($token);
return $this;
}
public function getRefreshToken(): array
public function accessToken(): OAuth2AccessToken
{
return $this->getTokens();
return $this->token;
}
public function accessToken(string $code, string $realm): array
public function saveOAuthToken(OAuth2AccessToken $token): void
{
$token = ($this->sdk->getOAuth2LoginHelper())->exchangeAuthorizationCodeForToken($code, $realm);
$obj = new \stdClass();
$obj->accessTokenKey = $token->getAccessToken();
$obj->refresh_token = $token->getRefreshToken();
$obj->accessTokenExpiresAt = Carbon::createFromFormat('Y/m/d H:i:s', $token->getAccessTokenExpiresAt())->timestamp; //@phpstan-ignore-line - QB phpdoc wrong types!!
$obj->refreshTokenExpiresAt = Carbon::createFromFormat('Y/m/d H:i:s', $token->getRefreshTokenExpiresAt())->timestamp; //@phpstan-ignore-line - QB phpdoc wrong types!!
return $this->getTokens();
$obj->realmID = $token->getRealmID();
$obj->baseURL = $token->getBaseURL();
$this->company->quickbooks = $obj;
$this->company->save();
}
private function getTokens()
{
$token = ($this->sdk->getOAuth2LoginHelper())->getAccessToken();
return $token;
// $access_token = $token->getAccessToken();
// $refresh_token = $token->getRefreshToken();
// $access_token_expires = $token->getAccessTokenExpiresAt();
// $refresh_token_expires = $token->getRefreshTokenExpiresAt();
// return compact('access_token', 'refresh_token','access_token_expires', 'refresh_token_expires');
}
public function refreshToken(): array
{
$token = ($this->sdk->getOAuth2LoginHelper())->refreshToken();
$this->sdk = $this->sdk->updateOAuth2Token($token);
return $this->getTokens();
}
public function handleCallbacks(array $data): void
{
}
public function totalRecords(string $entity): int
public function totalRecords(string $entity) //returns an array not int
{
nlog($this->sdk->Query("select count(*) from $entity"));
return $this->sdk->Query("select count(*) from $entity");
}
@ -81,39 +155,39 @@ final class SdkWrapper implements QuickbooksInterface
return (array) $this->sdk->Query($query, $start, $limit);
}
public function fetchRecords(string $entity, int $max = 1000): array
{
// public function fetchRecords(string $entity, int $max = 1000): array
// {
if(!in_array($entity, $this->entities)) {
return [];
}
// if(!in_array($entity, $this->entities)) {
// return [];
// }
$records = [];
$start = 0;
$limit = 100;
try {
$total = $this->totalRecords($entity);
$total = min($max, $total);
// $records = [];
// $start = 0;
// $limit = 100;
// try {
// $total = $this->totalRecords($entity);
// $total = min($max, $total);
// Step 3 & 4: Get chunks of records until the total required records are retrieved
do {
$limit = min(self::MAXRESULTS, $total - $start);
$recordsChunk = $this->queryData("select * from $entity", $start, $limit);
if(empty($recordsChunk)) {
break;
}
// // Step 3 & 4: Get chunks of records until the total required records are retrieved
// do {
// $limit = min(self::MAXRESULTS, $total - $start);
// $recordsChunk = $this->queryData("select * from $entity", $start, $limit);
// if(empty($recordsChunk)) {
// break;
// }
$records = array_merge($records, $recordsChunk);
$start += $limit;
} while ($start < $total);
if(empty($records)) {
throw new \Exception("No records retrieved!");
}
// $records = array_merge($records, $recordsChunk);
// $start += $limit;
// } while ($start < $total);
// if(empty($records)) {
// throw new \Exception("No records retrieved!");
// }
} catch (\Throwable $th) {
nlog("Fetch Quickbooks API Error: {$th->getMessage()}");
}
// } catch (\Throwable $th) {
// nlog("Fetch Quickbooks API Error: {$th->getMessage()}");
// }
return $records;
}
// return $records;
// }
}

View File

@ -1,89 +0,0 @@
<?php
namespace App\Services\Import\Quickbooks;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache;
use App\Services\Import\Quickbooks\Auth;
use App\Repositories\Import\Quickbooks\Contracts\RepositoryInterface;
use App\Services\Import\Quickbooks\Contracts\SdkInterface as QuickbooksInterface;
final class Service
{
private QuickbooksInterface $sdk;
public function __construct(QuickbooksInterface $quickbooks)
{
$this->sdk = $quickbooks;
}
public function getOAuth(): Auth
{
return new Auth($this->sdk);
}
public function getAccessToken(): array
{
return $this->getOAuth()->getAccessToken();
}
public function getRefreshToken(): array
{
// TODO: Check if token is Cached otherwise fetch a new one and Cache token and expire
return $this->getAccessToken();
}
/**
* fetch QuickBooks invoice records
* @param int $max The maximum records to fetch. Default 100
* @return Illuminate\Support\Collection;
*/
public function fetchInvoices(int $max = 100): Collection
{
return $this->fetchRecords('Invoice', $max) ;
}
/**
* fetch QuickBooks payment records
* @param int $max The maximum records to fetch. Default 100
* @return Illuminate\Support\Collection;
*/
public function fetchPayments(int $max = 100): Collection
{
return $this->fetchRecords('Payment', $max) ;
}
/**
* fetch QuickBooks product records
* @param int $max The maximum records to fetch. Default 100
* @return Illuminate\Support\Collection;
*/
public function fetchItems(int $max = 100): Collection
{
return $this->fetchRecords('Item', $max) ;
}
protected function fetchRecords(string $entity, $max = 100): Collection
{
return (self::RepositoryFactory($entity))->get($max);
}
private static function RepositoryFactory(string $entity): RepositoryInterface
{
return app("\\App\\Repositories\\Import\Quickbooks\\{$entity}Repository");
}
/**
* fetch QuickBooks customer records
* @param int $max The maximum records to fetch. Default 100
* @return Illuminate\Support\Collection;
*/
public function fetchCustomers(int $max = 100): Collection
{
return $this->fetchRecords('Customer', $max) ;
}
public function totalRecords(string $entity): int
{
return (self::RepositoryFactory($entity))->count();
}
}

View File

@ -6,7 +6,6 @@ use App\Http\Controllers\Auth\ResetPasswordController;
use App\Http\Controllers\Bank\NordigenController;
use App\Http\Controllers\Bank\YodleeController;
use App\Http\Controllers\BaseController;
use App\Http\Controllers\ImportQuickbooksController;
use App\Http\Controllers\ClientPortal\ApplePayDomainController;
use App\Http\Controllers\Gateways\Checkout3dsController;
use App\Http\Controllers\Gateways\GoCardlessController;