2022-08-14 04:40:13 +00:00
|
|
|
<?php
|
|
|
|
|
|
|
|
namespace Intuit;
|
|
|
|
|
2023-05-12 12:41:53 +00:00
|
|
|
use GuzzleHttp\Exception\ConnectException;
|
2022-08-14 04:40:13 +00:00
|
|
|
use Illuminate\Support\Arr;
|
|
|
|
use Illuminate\Support\Facades\Cache;
|
|
|
|
use Illuminate\Support\Facades\Log;
|
|
|
|
|
2023-05-12 12:41:53 +00:00
|
|
|
use Intuit\Exceptions\{ConnectionIssueException,InvalidQueryResultException};
|
|
|
|
use Intuit\Models\{ProviderToken};
|
|
|
|
use Intuit\Response\{Customer,Invoice,ListList};
|
2022-08-14 04:40:13 +00:00
|
|
|
|
|
|
|
final class API
|
|
|
|
{
|
|
|
|
// https://developer.intuit.com/app/developer/qbo/docs/learn/rest-api-features
|
|
|
|
// @todo implement wait - will get 429 when throttling occurs, wait 60s
|
|
|
|
// @todo 500 requests/min per realm
|
|
|
|
// @todo requests that take 120s will timeout
|
|
|
|
// @todo max entities is 1000, use pagination for more
|
|
|
|
private const LOGKEY = 'API';
|
|
|
|
private const CACHE_TIME = 60*60*12;
|
|
|
|
private const VERSION = 'v3';
|
|
|
|
private const MINOR_VERSION = 65;
|
|
|
|
private const CURLOPT_HEADER = FALSE;
|
|
|
|
|
|
|
|
private ProviderToken $token;
|
|
|
|
|
|
|
|
public function __construct(ProviderToken $token,bool $tryprod=FALSE)
|
|
|
|
{
|
|
|
|
$this->url = (config('app.env') == 'local' && ! $tryprod) ? 'https://sandbox-quickbooks.api.intuit.com' : 'https://quickbooks.api.intuit.com';
|
|
|
|
$this->token = $token;
|
|
|
|
|
|
|
|
Log::debug(sprintf('%s:Intuit API for id [%s]',static::LOGKEY,$token->realm_id));
|
|
|
|
}
|
|
|
|
|
2022-08-19 10:11:46 +00:00
|
|
|
/* STATIC */
|
|
|
|
|
|
|
|
public static function url(bool $tryprod=FALSE): string
|
|
|
|
{
|
|
|
|
return (config('app.env') == 'local' && ! $tryprod) ? 'https://app.sandbox.qbo.intuit.com/app' : 'https://app.qbo.intuit.com/app';
|
|
|
|
}
|
|
|
|
|
2022-08-14 04:40:13 +00:00
|
|
|
/**
|
|
|
|
* Convert an Array to Curl Headers
|
|
|
|
*
|
|
|
|
* @param array $header
|
|
|
|
* @return array Curl Headers
|
|
|
|
*/
|
|
|
|
private function convertHeaders(array $header): array
|
|
|
|
{
|
|
|
|
return collect($header)
|
|
|
|
->map(function($value,$key) { return sprintf('%s:%s',$key,$value); })
|
|
|
|
->values()
|
|
|
|
->toArray();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Call the API
|
|
|
|
*
|
|
|
|
* @param string $path
|
|
|
|
* @param array $parameters
|
|
|
|
* @return object|array
|
|
|
|
* @throws \Exception
|
|
|
|
*/
|
|
|
|
private function execute(string $path,array $parameters=[])
|
|
|
|
{
|
|
|
|
$url = sprintf('%s/%s/company/%s/%s',$this->url,self::VERSION,$this->token->realm_id,$path);
|
|
|
|
|
|
|
|
$method = Arr::get($parameters,'method','GET');
|
|
|
|
|
|
|
|
if ($parameters)
|
|
|
|
Arr::forget($parameters,'method');
|
|
|
|
|
|
|
|
// If we are passed an array, we'll do a normal post.
|
|
|
|
switch ($method) {
|
|
|
|
case 'GET':
|
|
|
|
$request = $this->prepareRequest(
|
|
|
|
$url,
|
|
|
|
$parameters,
|
|
|
|
[
|
|
|
|
'Accept' => 'application/json',
|
|
|
|
'Authorization' => sprintf('Bearer %s',$this->token->access_token),
|
|
|
|
'Content-Type' => 'application/json',
|
|
|
|
]
|
|
|
|
);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 'POST':
|
|
|
|
$request = $this->prepareRequestPost(
|
|
|
|
$url,
|
|
|
|
$parameters,
|
|
|
|
[
|
2022-08-18 13:18:14 +00:00
|
|
|
'Accept' => 'application/json',
|
|
|
|
'Authorization' => sprintf('Bearer %s',$this->token->access_token),
|
|
|
|
'Content-Type' => 'application/json',
|
2022-08-14 04:40:13 +00:00
|
|
|
]
|
|
|
|
);
|
|
|
|
break;
|
|
|
|
|
2022-08-18 13:18:14 +00:00
|
|
|
/*
|
2022-08-14 04:40:13 +00:00
|
|
|
case 'PUT':
|
|
|
|
$request = $this->prepareRequestPut(
|
|
|
|
$url,
|
|
|
|
$parameters,
|
|
|
|
[
|
|
|
|
'accept: application/json',
|
|
|
|
'Api-Request-Id: '.$request_id,
|
|
|
|
'Api-Signature: '.$signature,
|
|
|
|
]
|
|
|
|
);
|
|
|
|
break;
|
|
|
|
*/
|
|
|
|
|
|
|
|
default:
|
|
|
|
throw new \Exception(sprintf('Unknown method: %s',$method));
|
|
|
|
}
|
|
|
|
|
|
|
|
$key = md5($path.serialize($parameters));
|
|
|
|
|
|
|
|
//Cache::forget($key);
|
|
|
|
$result = Cache::remember($key,self::CACHE_TIME,function() use ($request,$url) {
|
|
|
|
try {
|
|
|
|
$response = curl_exec($request);
|
|
|
|
|
2023-05-12 12:41:53 +00:00
|
|
|
switch ($x=curl_getinfo($request,CURLINFO_HTTP_CODE)) {
|
2023-05-10 08:54:46 +00:00
|
|
|
case 0:
|
2023-05-12 12:41:53 +00:00
|
|
|
switch (curl_errno($request)) {
|
|
|
|
// DNS Resolving issue
|
|
|
|
case 6:
|
|
|
|
throw new ConnectionIssueException(sprintf('%s:%s',self::LOGKEY,curl_error($request)));
|
|
|
|
|
|
|
|
default:
|
|
|
|
dump(['getinfo' => curl_getinfo($request),'curlerror' => curl_error($request),'errno' => curl_errno($request)]);
|
|
|
|
abort(500,'Error 0');
|
|
|
|
}
|
|
|
|
|
2022-08-14 04:40:13 +00:00
|
|
|
case 400:
|
|
|
|
case 401:
|
|
|
|
case 403:
|
|
|
|
case 404:
|
2023-05-12 12:41:53 +00:00
|
|
|
dump([$xx=curl_getinfo($request),'response' => $response]);
|
2022-08-14 04:40:13 +00:00
|
|
|
|
|
|
|
throw new \Exception(sprintf('CURL exec returned %d: %s (%s)',$x,curl_error($request),serialize($xx)));
|
|
|
|
}
|
|
|
|
|
|
|
|
curl_close($request);
|
2023-05-10 08:54:46 +00:00
|
|
|
|
|
|
|
if (! $response)
|
|
|
|
throw new \Exception(sprintf('%s:Request to [%s] returned no data?',self::LOGKEY,$url),['code'=>$x]);
|
|
|
|
|
2022-08-14 04:40:13 +00:00
|
|
|
return json_decode(self::CURLOPT_HEADER ? substr($response,curl_getinfo($request,CURLINFO_HEADER_SIZE)) : $response);
|
|
|
|
|
2023-05-12 12:41:53 +00:00
|
|
|
} catch (ConnectException|ConnectionIssueException $e) {
|
|
|
|
throw new ConnectionIssueException($e->getMessage());
|
|
|
|
|
2022-08-14 04:40:13 +00:00
|
|
|
} catch (\Exception $e) {
|
|
|
|
dump(['error'=>$e->getMessage()]);
|
|
|
|
Log::error(sprintf('%s:Got an error while posting to [%s] (%s)',static::LOGKEY,$url,$e->getMessage()),['m'=>__METHOD__]);
|
|
|
|
|
|
|
|
curl_close($request);
|
|
|
|
throw new \Exception($e->getMessage());
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
return $result;
|
|
|
|
}
|
|
|
|
|
2023-05-12 12:41:53 +00:00
|
|
|
/**
|
|
|
|
* Get a list of our classes
|
|
|
|
*
|
|
|
|
* @param array $parameters
|
|
|
|
* @return ListList
|
|
|
|
* @throws \Exception
|
|
|
|
*/
|
|
|
|
public function getClasses(array $parameters=[]): ListList
|
|
|
|
{
|
|
|
|
Log::debug(sprintf('%s:Get a list of classes',static::LOGKEY));
|
|
|
|
$key = 'Class';
|
|
|
|
$parameters['query'] = 'select * from Class';
|
|
|
|
|
|
|
|
return new ListList($this->execute('query',$parameters),$key);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Find a customer by their email address
|
|
|
|
*
|
|
|
|
* @param string $id
|
|
|
|
* @param array $parameters
|
|
|
|
* @return Customer
|
|
|
|
* @throws InvalidQueryResultException
|
|
|
|
*/
|
|
|
|
public function getAccountQuery(string $id,array $parameters=[]): Customer
|
|
|
|
{
|
|
|
|
Log::debug(sprintf('%s:Get a specific account [%s]',static::LOGKEY,$id));
|
|
|
|
|
|
|
|
$parameters['query'] = sprintf("select * from Customer where PrimaryEmailAddr='%s'",$id);
|
|
|
|
|
|
|
|
$x = $this->execute('query',$parameters);
|
|
|
|
|
|
|
|
if ((! $x->QueryResponse) || (! $x->QueryResponse->Customer) || (count($x->QueryResponse->Customer) !== 1))
|
|
|
|
throw new InvalidQueryResultException(sprintf('%s:Query response malformed',self::LOGKEY));
|
|
|
|
|
|
|
|
return new Customer($x->QueryResponse);
|
|
|
|
}
|
|
|
|
|
2022-08-18 13:18:14 +00:00
|
|
|
/**
|
|
|
|
* Get a specific customer record
|
|
|
|
*
|
|
|
|
* @param int $id
|
|
|
|
* @param array $parameters
|
|
|
|
* @return Customer
|
|
|
|
* @throws \Exception
|
|
|
|
*/
|
|
|
|
public function getCustomer(int $id,array $parameters=[]): Customer
|
|
|
|
{
|
|
|
|
Log::debug(sprintf('%s:Get a specific customer [%d]',static::LOGKEY,$id));
|
|
|
|
|
|
|
|
return new Customer($this->execute('customer/'.$id,$parameters));
|
|
|
|
}
|
|
|
|
|
2022-08-25 01:22:24 +00:00
|
|
|
/**
|
2023-05-12 12:41:53 +00:00
|
|
|
* Get a list of our clients
|
2022-08-25 01:22:24 +00:00
|
|
|
*
|
|
|
|
* @param array $parameters
|
|
|
|
* @return ListList
|
|
|
|
* @throws \Exception
|
|
|
|
*/
|
2023-05-12 12:41:53 +00:00
|
|
|
public function getCustomers(array $parameters=[]): ListList
|
2022-08-25 01:22:24 +00:00
|
|
|
{
|
2023-05-12 12:41:53 +00:00
|
|
|
Log::debug(sprintf('%s:Get a list of customers',static::LOGKEY));
|
|
|
|
$key = 'Customer';
|
|
|
|
$parameters['query'] = 'select * from Customer';
|
2022-08-25 01:22:24 +00:00
|
|
|
|
|
|
|
return new ListList($this->execute('query',$parameters),$key);
|
|
|
|
}
|
|
|
|
|
2022-08-14 04:40:13 +00:00
|
|
|
/**
|
2023-05-12 12:41:53 +00:00
|
|
|
* Find an invoice by its Document Number
|
2022-08-14 04:40:13 +00:00
|
|
|
*
|
2023-05-12 12:41:53 +00:00
|
|
|
* @param string $id
|
2022-08-14 04:40:13 +00:00
|
|
|
* @param array $parameters
|
2023-05-12 12:41:53 +00:00
|
|
|
* @return Invoice
|
|
|
|
* @throws InvalidQueryResultException
|
2022-08-14 04:40:13 +00:00
|
|
|
*/
|
2023-05-12 12:41:53 +00:00
|
|
|
public function getInvoiceQuery(string $id,array $parameters=[]): Invoice
|
2022-08-14 04:40:13 +00:00
|
|
|
{
|
2023-05-12 12:41:53 +00:00
|
|
|
Log::debug(sprintf('%s:Get a specific invoice [%s]',static::LOGKEY,$id));
|
2022-08-14 04:40:13 +00:00
|
|
|
|
2023-05-12 12:41:53 +00:00
|
|
|
$parameters['query'] = sprintf("select * from Invoice where DocNumber='%s'",$id);
|
|
|
|
|
|
|
|
$x = $this->execute('query',$parameters);
|
|
|
|
|
|
|
|
if ((! $x->QueryResponse) || (! $x->QueryResponse->Invoice) || (count($x->QueryResponse->Invoice) !== 1))
|
|
|
|
throw new InvalidQueryResultException(sprintf('%s:Query response malformed',self::LOGKEY));
|
|
|
|
|
|
|
|
return new Invoice($x->QueryResponse);
|
2022-08-14 04:40:13 +00:00
|
|
|
}
|
|
|
|
|
2022-08-19 10:51:35 +00:00
|
|
|
/**
|
|
|
|
* Get a list of our invoices
|
|
|
|
*
|
|
|
|
* @param array $parameters
|
|
|
|
* @return ListList
|
|
|
|
* @throws \Exception
|
|
|
|
*/
|
|
|
|
public function getInvoices(array $parameters=[]): ListList
|
|
|
|
{
|
|
|
|
Log::debug(sprintf('%s:Get a list of invoices',static::LOGKEY));
|
2023-05-12 12:41:53 +00:00
|
|
|
$key = 'Invoice';
|
2022-08-19 10:51:35 +00:00
|
|
|
$parameters['query'] = 'select * from Invoice';
|
|
|
|
|
|
|
|
return new ListList($this->execute('query',$parameters),$key);
|
|
|
|
}
|
|
|
|
|
2022-08-25 01:22:24 +00:00
|
|
|
/**
|
|
|
|
* Get a list of our items
|
|
|
|
*
|
|
|
|
* @param array $parameters
|
|
|
|
* @return ListList
|
|
|
|
* @throws \Exception
|
|
|
|
*/
|
|
|
|
public function getItems(array $parameters=[]): ListList
|
|
|
|
{
|
|
|
|
Log::debug(sprintf('%s:Get a list of items',static::LOGKEY));
|
2023-05-12 12:41:53 +00:00
|
|
|
$key = 'Item';
|
2022-08-25 01:22:24 +00:00
|
|
|
$parameters['query'] = 'select * from Item';
|
|
|
|
|
|
|
|
return new ListList($this->execute('query',$parameters),$key);
|
|
|
|
}
|
|
|
|
|
2023-05-12 12:41:53 +00:00
|
|
|
/**
|
|
|
|
* Get a list of our invoices
|
|
|
|
*
|
|
|
|
* @param array $parameters
|
|
|
|
* @return ListList
|
|
|
|
* @throws \Exception
|
|
|
|
*/
|
|
|
|
public function getTaxCodes(array $parameters=[]): ListList
|
|
|
|
{
|
|
|
|
Log::debug(sprintf('%s:Get a list of taxes',static::LOGKEY));
|
|
|
|
$key = 'TaxCode';
|
|
|
|
$parameters['query'] = 'select * from Taxcode';
|
|
|
|
|
|
|
|
return new ListList($this->execute('query',$parameters),$key);
|
|
|
|
}
|
|
|
|
|
2022-08-14 04:40:13 +00:00
|
|
|
/**
|
|
|
|
* Setup the API call
|
|
|
|
*
|
2022-08-25 01:22:24 +00:00
|
|
|
* @param string $url
|
2022-08-14 04:40:13 +00:00
|
|
|
* @param array $parameters
|
|
|
|
* @param array $headers
|
|
|
|
* @return \CurlHandle
|
|
|
|
*/
|
2022-08-18 13:18:14 +00:00
|
|
|
private function prepareRequest(string $url,array $parameters=[],array $headers=[]): \CurlHandle
|
2022-08-14 04:40:13 +00:00
|
|
|
{
|
|
|
|
$request = curl_init();
|
|
|
|
|
|
|
|
curl_setopt($request,CURLOPT_HEADER,self::CURLOPT_HEADER); // debugging set this to TRUE, but it affects our body.
|
|
|
|
|
|
|
|
curl_setopt($request,CURLOPT_URL,$url.'?'.http_build_query(array_merge($parameters,['minorversion'=>self::MINOR_VERSION])));
|
|
|
|
curl_setopt($request,CURLOPT_RETURNTRANSFER,TRUE);
|
|
|
|
curl_setopt($request,CURLOPT_CUSTOMREQUEST,'GET');
|
|
|
|
curl_setopt($request,CURLINFO_HEADER_OUT,TRUE);
|
|
|
|
curl_setopt($request,CURLOPT_SSL_VERIFYPEER,TRUE);
|
|
|
|
curl_setopt($request,CURLOPT_CONNECTTIMEOUT,15);
|
|
|
|
curl_setopt($request,CURLOPT_TIMEOUT,15);
|
|
|
|
|
|
|
|
if ($headers)
|
|
|
|
curl_setopt($request,CURLOPT_HTTPHEADER,$this->convertHeaders($headers));
|
|
|
|
|
|
|
|
return $request;
|
|
|
|
}
|
2022-08-18 13:18:14 +00:00
|
|
|
|
|
|
|
private function prepareRequestPost(string $url,array $parameters=[],array $headers=[]): \CurlHandle
|
|
|
|
{
|
|
|
|
$request = $this->prepareRequest($url,$parameters,$headers);
|
|
|
|
|
|
|
|
curl_setopt($request,CURLOPT_CUSTOMREQUEST,'POST');
|
|
|
|
curl_setopt($request,CURLOPT_POSTFIELDS,json_encode($parameters));
|
|
|
|
|
|
|
|
return $request;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2023-05-12 12:41:53 +00:00
|
|
|
* Create/Update an invoice
|
|
|
|
*
|
|
|
|
* @param array $parameters
|
|
|
|
* @return Invoice
|
|
|
|
* @throws \Exception
|
|
|
|
*/
|
|
|
|
public function updateInvoice(array $parameters=[]): Invoice
|
|
|
|
{
|
|
|
|
Log::debug(sprintf('%s:Update invoice',static::LOGKEY),['params'=>$parameters]);
|
|
|
|
|
|
|
|
return new Invoice($this->execute('invoice',array_merge($parameters,['method'=>'POST'])));
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Create/Update a customer
|
2022-08-18 13:18:14 +00:00
|
|
|
*
|
|
|
|
* @param array $parameters
|
|
|
|
* @return Customer
|
|
|
|
* @throws \Exception
|
|
|
|
*/
|
|
|
|
public function updateCustomer(array $parameters=[]): Customer
|
|
|
|
{
|
|
|
|
Log::debug(sprintf('%s:Update customer',static::LOGKEY),['params'=>$parameters]);
|
|
|
|
|
|
|
|
return new Customer($this->execute('customer',array_merge($parameters,['method'=>'POST'])));
|
|
|
|
}
|
2022-08-14 04:40:13 +00:00
|
|
|
}
|