diff --git a/src/API.php b/src/API.php new file mode 100644 index 0000000..c5a4b90 --- /dev/null +++ b/src/API.php @@ -0,0 +1,187 @@ +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; + } +} \ No newline at end of file diff --git a/src/Models/Customer.php b/src/Models/Customer.php new file mode 100644 index 0000000..a032048 --- /dev/null +++ b/src/Models/Customer.php @@ -0,0 +1,74 @@ + '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)); + } + } +} \ No newline at end of file diff --git a/src/Models/ProviderToken.php b/src/Models/ProviderToken.php new file mode 100644 index 0000000..9825eb7 --- /dev/null +++ b/src/Models/ProviderToken.php @@ -0,0 +1,39 @@ +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); + } +} \ No newline at end of file diff --git a/src/Response/Base.php b/src/Response/Base.php new file mode 100644 index 0000000..c6d499d --- /dev/null +++ b/src/Response/Base.php @@ -0,0 +1,159 @@ +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; }); + } +} \ No newline at end of file diff --git a/src/Response/ListList.php b/src/Response/ListList.php new file mode 100644 index 0000000..7fdd618 --- /dev/null +++ b/src/Response/ListList.php @@ -0,0 +1,11 @@ +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; + } +} \ No newline at end of file