Implement Customer List
This commit is contained in:
parent
f7d69d7f04
commit
bbc747621e
187
src/API.php
Normal file
187
src/API.php
Normal file
@ -0,0 +1,187 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Intuit;
|
||||||
|
|
||||||
|
use Illuminate\Support\Arr;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
|
use Intuit\Models\ProviderToken;
|
||||||
|
use Intuit\Response\ListList;
|
||||||
|
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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,
|
||||||
|
[
|
||||||
|
'accept: application/json',
|
||||||
|
'Api-Request-Id: '.$request_id,
|
||||||
|
'Api-Signature: '.$signature,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
switch($x=curl_getinfo($request,CURLINFO_HTTP_CODE)) {
|
||||||
|
case 400:
|
||||||
|
case 401:
|
||||||
|
case 403:
|
||||||
|
case 404:
|
||||||
|
dump([$xx=curl_getinfo($request),'response'=>$response]);
|
||||||
|
|
||||||
|
throw new \Exception(sprintf('CURL exec returned %d: %s (%s)',$x,curl_error($request),serialize($xx)));
|
||||||
|
}
|
||||||
|
|
||||||
|
curl_close($request);
|
||||||
|
return json_decode(self::CURLOPT_HEADER ? substr($response,curl_getinfo($request,CURLINFO_HEADER_SIZE)) : $response);
|
||||||
|
|
||||||
|
} 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a list of our clients
|
||||||
|
*
|
||||||
|
* select * from Account where Metadata.CreateTime > '2014-12-31'
|
||||||
|
*
|
||||||
|
* @param array $parameters
|
||||||
|
* @return ListList
|
||||||
|
* @throws \Exception
|
||||||
|
*/
|
||||||
|
public function getCustomers(array $parameters=[]): ListList
|
||||||
|
{
|
||||||
|
Log::debug(sprintf('%s:Get a list of customers',static::LOGKEY));
|
||||||
|
$key = 'customers';
|
||||||
|
$parameters['query'] = 'select * from Customer';
|
||||||
|
|
||||||
|
return new ListList($this->execute('query',$parameters),$key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup the API call
|
||||||
|
*
|
||||||
|
* @param $url
|
||||||
|
* @param array $parameters
|
||||||
|
* @param array $headers
|
||||||
|
* @return \CurlHandle
|
||||||
|
*/
|
||||||
|
private function prepareRequest($url,array $parameters=[],array $headers=[]): \CurlHandle
|
||||||
|
{
|
||||||
|
$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;
|
||||||
|
}
|
||||||
|
}
|
74
src/Models/Customer.php
Normal file
74
src/Models/Customer.php
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Intuit\Models;
|
||||||
|
|
||||||
|
use Illuminate\Support\Arr;
|
||||||
|
use Intuit\Traits\CompareAttributes;
|
||||||
|
use Jenssegers\Model\Model;
|
||||||
|
|
||||||
|
/*
|
||||||
|
+"Customer": {#1760
|
||||||
|
+"Taxable": false
|
||||||
|
+"BillAddr": {#1758
|
||||||
|
+"Id": "2"
|
||||||
|
+"Line1": "25 Stone Drive"
|
||||||
|
+"City": "Sydney"
|
||||||
|
+"CountrySubDivisionCode": "NSW"
|
||||||
|
+"PostalCode": "2001"
|
||||||
|
+"Lat": "-33.737492"
|
||||||
|
+"Long": "151.195909"
|
||||||
|
}
|
||||||
|
+"Job": false
|
||||||
|
+"BillWithParent": false
|
||||||
|
+"Balance": 1650.0
|
||||||
|
+"BalanceWithJobs": 1650.0
|
||||||
|
+"CurrencyRef": {#1761
|
||||||
|
+"value": "AUD"
|
||||||
|
+"name": "Australian Dollar"
|
||||||
|
}
|
||||||
|
+"PreferredDeliveryMethod": "None"
|
||||||
|
+"IsProject": false
|
||||||
|
+"domain": "QBO"
|
||||||
|
+"sparse": false
|
||||||
|
+"Id": "1"
|
||||||
|
+"SyncToken": "0"
|
||||||
|
+"MetaData": {#1762
|
||||||
|
+"CreateTime": "2018-02-21T20:02:17-08:00"
|
||||||
|
+"LastUpdatedTime": "2018-03-22T13:36:42-07:00"
|
||||||
|
}
|
||||||
|
+"GivenName": "Adwin"
|
||||||
|
+"FamilyName": "Ko"
|
||||||
|
+"FullyQualifiedName": "Adwin Ko"
|
||||||
|
+"CompanyName": "Ko International Ltd"
|
||||||
|
+"DisplayName": "Adwin Ko"
|
||||||
|
+"PrintOnCheckName": "Adwin Ko"
|
||||||
|
+"Active": true
|
||||||
|
}
|
||||||
|
+"time": "2022-08-13T21:15:37.254-07:00"
|
||||||
|
*/
|
||||||
|
|
||||||
|
final class Customer extends Model
|
||||||
|
{
|
||||||
|
use CompareAttributes;
|
||||||
|
|
||||||
|
public function __get($key) {
|
||||||
|
$keymap = [
|
||||||
|
'id' => 'Id',
|
||||||
|
'active' => 'Active',
|
||||||
|
'companyname' => 'CompanyName',
|
||||||
|
'fullname' => 'FullyQualifiedName',
|
||||||
|
'ref' => 'ResaleNum',
|
||||||
|
'synctoken' => 'SyncToken',
|
||||||
|
];
|
||||||
|
|
||||||
|
switch ($key) {
|
||||||
|
case 'created_at':
|
||||||
|
return object_get($this->getAttribute('MetaData'),'CreateTime');
|
||||||
|
case 'updated_at':
|
||||||
|
return object_get($this->getAttribute('MetaData'),'LastUpdatedTime');
|
||||||
|
|
||||||
|
default:
|
||||||
|
return parent::__get(Arr::get($keymap,$key,$key));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
39
src/Models/ProviderToken.php
Normal file
39
src/Models/ProviderToken.php
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Intuit\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Laravel\Socialite\Facades\Socialite;
|
||||||
|
|
||||||
|
class ProviderToken extends Model
|
||||||
|
{
|
||||||
|
protected $dates = [
|
||||||
|
'access_token_expires_at',
|
||||||
|
'refresh_token_expires_at',
|
||||||
|
];
|
||||||
|
|
||||||
|
/* ATTRIBUTES */
|
||||||
|
|
||||||
|
public function getAccessTokenAttribute($value): ?string
|
||||||
|
{
|
||||||
|
if (! $this->hasAccessTokenExpired())
|
||||||
|
return $value;
|
||||||
|
|
||||||
|
// Auto refresh the token
|
||||||
|
$this->refreshToken();
|
||||||
|
|
||||||
|
return (! $this->hasAccessTokenExpired()) ? $this->getRawOriginal('access_token') : NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* METHODS */
|
||||||
|
|
||||||
|
public function hasAccessTokenExpired(): bool
|
||||||
|
{
|
||||||
|
return $this->access_token_expires_at->isPast();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function refreshToken(): bool
|
||||||
|
{
|
||||||
|
return Socialite::with($this->provider->name)->refreshtoken($this);
|
||||||
|
}
|
||||||
|
}
|
159
src/Response/Base.php
Normal file
159
src/Response/Base.php
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Intuit\Response;
|
||||||
|
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
use Illuminate\Support\Facades\App;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
|
use Intuit\Models\Customer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This parent class handles responses received from Intuit
|
||||||
|
*
|
||||||
|
* @note: This class is used for events not specifically created.
|
||||||
|
*/
|
||||||
|
abstract class Base implements \JsonSerializable, \Countable, \ArrayAccess, \Iterator
|
||||||
|
{
|
||||||
|
protected const LOGKEY = 'RB-';
|
||||||
|
|
||||||
|
protected Collection $_data;
|
||||||
|
protected ?string $_type;
|
||||||
|
protected int $startPosition;
|
||||||
|
protected int $maxResults;
|
||||||
|
|
||||||
|
private ?int $counter = NULL;
|
||||||
|
protected const TYPES = [
|
||||||
|
'customers',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default Constructor Setup
|
||||||
|
*
|
||||||
|
* @param object $response
|
||||||
|
* @param string $type
|
||||||
|
* @throws \Exception
|
||||||
|
*/
|
||||||
|
public function __construct(object $response,string $type)
|
||||||
|
{
|
||||||
|
if (! in_array($type,self::TYPES))
|
||||||
|
throw new \Exception('Unknown data type: '.$type);
|
||||||
|
|
||||||
|
if (object_get($response,'time'))
|
||||||
|
unset($response->time);
|
||||||
|
|
||||||
|
$this->_data = $this->data($response,$type);
|
||||||
|
|
||||||
|
// This is only for child classes
|
||||||
|
if (get_class($this) == Base::class) {
|
||||||
|
Log::debug(sprintf('%s:Intuit RESPONSE Initialised [%s]',static::LOGKEY,get_class($this)),['m'=>__METHOD__]);
|
||||||
|
|
||||||
|
if (App::environment() == 'dev')
|
||||||
|
file_put_contents('/tmp/response',print_r($this,TRUE),FILE_APPEND);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ABSTRACT */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When we json_encode this object, this is the data that will be returned
|
||||||
|
*/
|
||||||
|
public function jsonSerialize(): mixed
|
||||||
|
{
|
||||||
|
return $this->_data ?: new \stdClass;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function current(): mixed
|
||||||
|
{
|
||||||
|
return $this->_data[$this->counter];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function next(): void
|
||||||
|
{
|
||||||
|
$this->counter++;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function key(): mixed
|
||||||
|
{
|
||||||
|
return $this->counter;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function valid(): bool
|
||||||
|
{
|
||||||
|
return isset($this->_data[$this->counter]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rewind(): void
|
||||||
|
{
|
||||||
|
$this->counter = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function offsetExists(mixed $offset): bool
|
||||||
|
{
|
||||||
|
return $this->_data->has($offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function offsetGet(mixed $offset): mixed
|
||||||
|
{
|
||||||
|
return $this->_data->get($offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function offsetSet(mixed $offset, mixed $value): void
|
||||||
|
{
|
||||||
|
throw new \Exception('Method not implemented: '.__METHOD__);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function offsetUnset(mixed $offset): void
|
||||||
|
{
|
||||||
|
$this->_data->forget($offset);
|
||||||
|
|
||||||
|
// Rekey the collection
|
||||||
|
$this->_data = $this->_data->values();
|
||||||
|
|
||||||
|
// Reset the counter if we have deleted a value before it
|
||||||
|
if ($offset < $this->counter)
|
||||||
|
$this->counter--;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function count(): int
|
||||||
|
{
|
||||||
|
return $this->_data->count();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* METHODS */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert our response into a collection of the appropriate model
|
||||||
|
*
|
||||||
|
* @param object $response
|
||||||
|
* @param string $type
|
||||||
|
* @return Collection
|
||||||
|
* @throws \Exception
|
||||||
|
*/
|
||||||
|
private function data(object $response,string $type): Collection
|
||||||
|
{
|
||||||
|
switch ($type) {
|
||||||
|
case 'customers':
|
||||||
|
$data = collect(Customer::hydrate($response->QueryResponse->Customer));
|
||||||
|
$this->startPosition = $response->QueryResponse->startPosition;
|
||||||
|
$this->maxResults = $response->QueryResponse->maxResults;
|
||||||
|
break;
|
||||||
|
|
||||||
|
default: throw new \Exception('Unknown object type: '.$this->_type);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search for an item in the result
|
||||||
|
*
|
||||||
|
* @param string $key
|
||||||
|
* @param mixed $value
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function search(string $key, mixed $value): mixed
|
||||||
|
{
|
||||||
|
return $this->_data->search(function($item) use ($key,$value) { return $item->{$key} == $value; });
|
||||||
|
}
|
||||||
|
}
|
11
src/Response/ListList.php
Normal file
11
src/Response/ListList.php
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Intuit\Response;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is a Generic Intuit Response to API calls
|
||||||
|
*/
|
||||||
|
class ListList extends Base
|
||||||
|
{
|
||||||
|
protected const LOGKEY = 'RLI';
|
||||||
|
}
|
49
src/Traits/CompareAttributes.php
Normal file
49
src/Traits/CompareAttributes.php
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This trait enables a model to be updated and to provide the changes
|
||||||
|
*/
|
||||||
|
namespace Intuit\Traits;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Concerns\HasAttributes;
|
||||||
|
|
||||||
|
trait CompareAttributes
|
||||||
|
{
|
||||||
|
use HasAttributes;
|
||||||
|
|
||||||
|
/* OVERRIDES */
|
||||||
|
|
||||||
|
/* Since we use Jenson model, with HasAttributes, we need to add and override some basic dependancies */
|
||||||
|
|
||||||
|
public function getIncrementing() { return FALSE; }
|
||||||
|
public function usesTimestamps() { return FALSE; }
|
||||||
|
public function getAttribute($key) {
|
||||||
|
return (array_key_exists($key,$this->attributes)) ? $this->attributes[$key] : NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Only record a change if it has changed
|
||||||
|
*
|
||||||
|
* @param $key
|
||||||
|
* @param $value
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __set($key,$value) {
|
||||||
|
if (array_key_exists($key,$this->attributes) && (! $this->isSame($key,$value)))
|
||||||
|
parent::__set($key,$value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check to see if a value has changed
|
||||||
|
*
|
||||||
|
* @param string $key
|
||||||
|
* @param $tgt
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
private function isSame(string $key,$tgt): bool
|
||||||
|
{
|
||||||
|
$src = $this->getAttribute($key);
|
||||||
|
|
||||||
|
return (is_object($src) || is_array($tgt)) ? count(array_diff_assoc((array)$src,(array)$tgt)) === 0 : $src === $tgt;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user