diff --git a/src/API.php b/src/API.php index f3f0d39..885fc09 100644 --- a/src/API.php +++ b/src/API.php @@ -2,12 +2,14 @@ namespace Intuit; +use GuzzleHttp\Exception\ConnectException; use Illuminate\Support\Arr; use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Log; -use Intuit\Models\ProviderToken; -use Intuit\Response\{Customer,ListList}; +use Intuit\Exceptions\{ConnectionIssueException,InvalidQueryResultException}; +use Intuit\Models\{ProviderToken}; +use Intuit\Response\{Customer,Invoice,ListList}; final class API { @@ -121,13 +123,23 @@ final class API try { $response = curl_exec($request); - switch($x=curl_getinfo($request,CURLINFO_HTTP_CODE)) { + switch ($x=curl_getinfo($request,CURLINFO_HTTP_CODE)) { case 0: + 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'); + } + case 400: case 401: case 403: case 404: - dump([$xx=curl_getinfo($request),'response'=>$response]); + dump([$xx=curl_getinfo($request),'response' => $response]); throw new \Exception(sprintf('CURL exec returned %d: %s (%s)',$x,curl_error($request),serialize($xx))); } @@ -139,6 +151,9 @@ final class API return json_decode(self::CURLOPT_HEADER ? substr($response,curl_getinfo($request,CURLINFO_HEADER_SIZE)) : $response); + } catch (ConnectException|ConnectionIssueException $e) { + throw new ConnectionIssueException($e->getMessage()); + } 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__]); @@ -151,6 +166,44 @@ final class API return $result; } + /** + * 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); + } + /** * Get a specific customer record * @@ -166,22 +219,6 @@ final class API return new Customer($this->execute('customer/'.$id,$parameters)); } - /** - * 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 = 'classes'; - $parameters['query'] = 'select * from Class'; - - return new ListList($this->execute('query',$parameters),$key); - } - /** * Get a list of our clients * @@ -192,12 +229,34 @@ final class API public function getCustomers(array $parameters=[]): ListList { Log::debug(sprintf('%s:Get a list of customers',static::LOGKEY)); - $key = 'customers'; + $key = 'Customer'; $parameters['query'] = 'select * from Customer'; return new ListList($this->execute('query',$parameters),$key); } + /** + * Find an invoice by its Document Number + * + * @param string $id + * @param array $parameters + * @return Invoice + * @throws InvalidQueryResultException + */ + public function getInvoiceQuery(string $id,array $parameters=[]): Invoice + { + Log::debug(sprintf('%s:Get a specific invoice [%s]',static::LOGKEY,$id)); + + $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); + } + /** * Get a list of our invoices * @@ -208,7 +267,7 @@ final class API public function getInvoices(array $parameters=[]): ListList { Log::debug(sprintf('%s:Get a list of invoices',static::LOGKEY)); - $key = 'invoices'; + $key = 'Invoice'; $parameters['query'] = 'select * from Invoice'; return new ListList($this->execute('query',$parameters),$key); @@ -224,12 +283,28 @@ final class API public function getItems(array $parameters=[]): ListList { Log::debug(sprintf('%s:Get a list of items',static::LOGKEY)); - $key = 'items'; + $key = 'Item'; $parameters['query'] = 'select * from Item'; return new ListList($this->execute('query',$parameters),$key); } + /** + * 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); + } + /** * Setup the API call * @@ -269,7 +344,21 @@ final class API } /** - * Update a customer + * 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 * * @param array $parameters * @return Customer diff --git a/src/Exceptions/ConnectionIssueException.php b/src/Exceptions/ConnectionIssueException.php new file mode 100644 index 0000000..3162e8b --- /dev/null +++ b/src/Exceptions/ConnectionIssueException.php @@ -0,0 +1,9 @@ +onQueue('intuit'); - $this->customer = $customer; + $this->o = $item; $this->to = $to->withoutRelations(); - $this->forceprod = $forcprod; - } + } - /** - * Execute the job. - * - * @return void - */ - public function handle() - { - Config::set('site',$this->to->site); + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + $api = $this->to->API(); + + // Get the original account, so we can get the provider sync details + $o = $this->to->provider->accounts->where('id',$x=(int)substr($this->o->ResaleNum,3)); + $o = ($o->count() === 1) ? $o->pop() : AppAccount::findOrFail($x); - $api = $this->to->provider->API($this->to,$this->forceprod); $updated = $api->updateCustomer(array_merge( - $this->customer->getDirty(), - [ - 'Id'=>$this->customer->id, - 'SyncToken'=>$this->customer->synctoken, - ])); + $this->o->getDirty(), + $o->providers->count() + ? + [ + 'Id'=>$o->pivot->ref, + 'SyncToken'=>$o->pivot->synctoken, + ] + : + [] + )); - if (($x=$this->to->provider->accounts->where('pivot.ref',$updated->id))->count() === 1) { - $ao = $x->pop(); - - $ao->providers()->syncWithoutDetaching([ + if ($updated instanceof \Intuit\Response\Customer) { + $o->providers()->syncWithoutDetaching([ $this->to->provider->id => [ 'ref' => $updated->id, 'synctoken' => $updated->synctoken, 'created_at'=>Carbon::create($updated->created_at), 'updated_at'=>Carbon::create($updated->updated_at), - 'site_id'=>$ao->site_id, // @todo See if we can have this handled automatically + 'site_id'=>$o->site_id, // @todo See if we can have this handled automatically ], ]); - Log::info(sprintf('%s:Updated account [%s] (%s:%s), synctoken now [%s]',self::LOGKEY,$ao->sid,$updated->id,$updated->DisplayName,$updated->synctoken)); + Log::info(sprintf('%s:Updated account [%s:%s] (%s:%s), synctoken now [%s]',self::LOGKEY,$this->to->provider->name,$o->sid,$updated->id,$updated->DisplayName,$updated->synctoken)); } else { - Log::error(sprintf('%s:Unable to update account refer for [%s:%s]',self::LOGKEY,$updated->id,$updated->DisplayName)); + Log::error(sprintf('%s:Unable to update account with provider [%s] for [%s:%s]',self::LOGKEY,$this->to->provider->name,$updated->id,$updated->DisplayName),['updated'=>get_class($updated)]); } - } -} + } +} \ No newline at end of file diff --git a/src/Jobs/AccountingInvoiceUpdate.php b/src/Jobs/AccountingInvoiceUpdate.php new file mode 100644 index 0000000..3e3b836 --- /dev/null +++ b/src/Jobs/AccountingInvoiceUpdate.php @@ -0,0 +1,81 @@ +onQueue('intuit'); + + $this->o = $item; + $this->to = $to->withoutRelations(); + } + + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + $api = $this->to->API(); + + // Get the original invoice, so we can get the provider sync details + $o = $this->to->provider->invoices->where('id',$this->o->DocNumber); + $o = ($o->count() === 1) ? $o->pop() : AppInvoice::findOrFail($this->o->DocNumber); + + $updated = $api->updateInvoice(array_merge( + $this->o->getDirty(), + $o->providers->count() + ? + [ + 'Id'=>$o->pivot->ref, + 'SyncToken'=>$o->pivot->synctoken, + ] + : + [] + )); + + if ($updated instanceof \Intuit\Response\Invoice) { + $o->providers()->syncWithoutDetaching([ + $this->to->provider->id => [ + 'ref' => $updated->id, + 'synctoken' => $updated->synctoken, + 'created_at'=>Carbon::create($updated->created_at), + 'updated_at'=>Carbon::create($updated->updated_at), + 'site_id'=>$o->site_id, // @todo See if we can have this handled automatically + ], + ]); + + Log::info(sprintf('%s:Updated invoice [%s:%s] (%s:%s), synctoken now [%s]',self::LOGKEY,$this->to->provider->name,$o->sid,$updated->id,$updated->DocNumber,$updated->synctoken)); + + } else { + Log::error(sprintf('%s:Unable to update invoice with provider [%s] for [%s:%s]',self::LOGKEY,$this->to->provider->name,$updated->id,$updated->DisplayName),['updated'=>get_class($updated)]); + } + } +} \ No newline at end of file diff --git a/src/Models/Taxcode.php b/src/Models/Taxcode.php new file mode 100644 index 0000000..4be3738 --- /dev/null +++ b/src/Models/Taxcode.php @@ -0,0 +1,66 @@ + {#2054 + +"TaxRateRef": {#2055 + +"value": "23" + +"name": "GST (sales)" + } + +"TaxTypeApplicable": "TaxOnAmount" + +"TaxOrder": 0 + } + ] + } + +"PurchaseTaxRateList": {#2056 + +"TaxRateDetail": [] + } + +"TaxCodeConfigType": "SYSTEM_GENERATED" + +"domain": "QBO" + +"sparse": false + +"Id": "14" + +"SyncToken": "0" + +"MetaData": {#2057 + +"CreateTime": "2023-05-10T01:05:41-07:00" + +"LastUpdatedTime": "2023-05-10T01:05:41-07:00" + } + } + */ + +final class Taxcode extends Model +{ + use CompareAttributes; + + public function __get($key) { + $keymap = [ + 'id' => 'Id', + 'synctoken' => 'SyncToken', + 'name' => 'Name', + ]; + + 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/Response/Base.php b/src/Response/Base.php index bce55e0..2e369a3 100644 --- a/src/Response/Base.php +++ b/src/Response/Base.php @@ -34,6 +34,10 @@ abstract class Base implements \JsonSerializable } } + public function __get($key) { + return $this->_model->__get($key); + } + /* ABSTRACT */ /** diff --git a/src/Response/Customer.php b/src/Response/Customer.php index 80bc82d..f72af44 100644 --- a/src/Response/Customer.php +++ b/src/Response/Customer.php @@ -2,6 +2,8 @@ namespace Intuit\Response; +use Intuit\Models\Customer as CustomerModel; + /** * This is a Customer Intuit Response to API calls */ @@ -16,10 +18,6 @@ class Customer extends Base if (object_get($response,'time')) unset($response->time); - $this->_model = new \Intuit\Models\Customer((array)$response->Customer); - } - - public function __get($key) { - return $this->_model->__get($key); + $this->_model = new CustomerModel((array)$response->Customer); } } \ No newline at end of file diff --git a/src/Response/Invoice.php b/src/Response/Invoice.php new file mode 100644 index 0000000..2542f64 --- /dev/null +++ b/src/Response/Invoice.php @@ -0,0 +1,23 @@ +time); + + $this->_model = new InvoiceModel((array)$response->Invoice); + } +} \ No newline at end of file diff --git a/src/Response/ListList.php b/src/Response/ListList.php index bfdc666..55904fe 100644 --- a/src/Response/ListList.php +++ b/src/Response/ListList.php @@ -7,7 +7,7 @@ use Illuminate\Support\Collection; use Illuminate\Support\Facades\App; use Illuminate\Support\Facades\Log; -use Intuit\Models\{Category,Customer,Invoice,Item}; +use Intuit\Models\{Category,Customer,Invoice,Item,Taxcode}; /** * This is a Generic Intuit Response to API calls that produces a list of objects @@ -23,10 +23,11 @@ class ListList extends Base implements \Countable, \ArrayAccess, \Iterator private ?int $counter = NULL; protected const TYPES = [ - 'classes' => 'Class', - 'customers' => 'Customer', - 'invoices' => 'Invoice', - 'items' => 'Item', + 'Class' => Category::class, + 'Customer' => Customer::class, + 'Invoice' => Invoice::class, + 'Item' => Item::class, + 'TaxCode' => Taxcode::class, ]; /** @@ -49,7 +50,7 @@ class ListList extends Base implements \Countable, \ArrayAccess, \Iterator $this->_data = $this->data($response,$type); // This is only for child classes - if (get_class($this) == Base::class) { + 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') @@ -66,7 +67,7 @@ class ListList extends Base implements \Countable, \ArrayAccess, \Iterator if (count($args) !== 2) throw new \BadMethodCallException(sprintf('Pluck requires to arguments %s::%s()',get_class($this),$name)); - return $this->_data->map(function($item) use ($args) { return [$item->{$args[1]} => $item->{$args[0]}]; })->flatMap(function($item) { return $item; }); + return collect(array_replace(...$this->_data->map(function($item) use ($args) { return [$item->{$args[1]} => $item->{$args[0]}]; }))); default: throw new \BadMethodCallException(sprintf('Call to undefined method %s::%s()',get_class($this),$name)); @@ -142,27 +143,33 @@ class ListList extends Base implements \Countable, \ArrayAccess, \Iterator */ private function data(object $response,string $type): Collection { - if (! ($x=object_get($response->QueryResponse,Arr::get(self::TYPES,$type)))) + if (! ($x=object_get($response->QueryResponse,$type))) return collect(); - switch ($type) { - case 'classes': + switch (Arr::get(self::TYPES,$type)) { + case Category::class: $data = collect(Category::hydrate($x)); break; - case 'customers': + case Customer::class: $data = collect(Customer::hydrate($x)); break; - case 'invoices': + case Invoice::class: $data = collect(Invoice::hydrate($x)); break; - case 'items': + case Item::class: $data = collect(Item::hydrate($x)); break; - default: throw new \Exception('Unknown object type: '.$this->_type); + case Taxcode::class: + $data = collect(Taxcode::hydrate($x)); + break; + + default: + Log::error(sprintf('%s:Unknown object type: %s',self::LOGKEY,$type)); + throw new \Exception(sprintf('%s:Unknown object type: %s',self::LOGKEY,$type)); } $this->startPosition = $response->QueryResponse->startPosition; diff --git a/src/Response/Taxcode.php b/src/Response/Taxcode.php new file mode 100644 index 0000000..264cadd --- /dev/null +++ b/src/Response/Taxcode.php @@ -0,0 +1,23 @@ +time); + + $this->_model = new TaxModel((array)$response->Tax); + } +} \ No newline at end of file