├── .gitignore ├── README.md ├── composer.json ├── phpunit.xml ├── src ├── Client.php ├── Contracts │ ├── Module.php │ └── ProvidesModules.php ├── Exceptions │ ├── AuthException.php │ ├── ErrorResponseException.php │ └── InvoiceMustBeLoaded.php ├── Mixins │ └── ProvidesModules.php ├── Models │ ├── BankAccount.php │ ├── BankAccount │ │ └── Statement.php │ ├── BankRule.php │ ├── BankTransaction.php │ ├── BaseCurrencyAdjustment.php │ ├── Bill.php │ ├── Bill │ │ └── Payment.php │ ├── ChartOfAccount.php │ ├── ChartOfAccount │ │ └── Transaction.php │ ├── Claimant.php │ ├── Comment.php │ ├── Contact.php │ ├── Contact │ │ ├── Address.php │ │ └── Person.php │ ├── CreditNote.php │ ├── CreditNote │ │ └── Invoice.php │ ├── CreditsApplied.php │ ├── CustomerPayment.php │ ├── CustomerPayment │ │ └── Refund.php │ ├── Document.php │ ├── Estimate.php │ ├── Estimate │ │ └── Template.php │ ├── Expense.php │ ├── Invoice.php │ ├── Invoice │ │ ├── CreditsApplied.php │ │ ├── Payment.php │ │ └── Template.php │ ├── Item.php │ ├── Journal.php │ ├── Model.php │ ├── Organization.php │ ├── Payment.php │ ├── Project.php │ ├── Project │ │ ├── Invoice.php │ │ ├── Task.php │ │ ├── TimeEntry.php │ │ └── User.php │ ├── PurchaseOrder.php │ ├── RecordCollection.php │ ├── RecurringExpense.php │ ├── RecurringExpense │ │ └── Expense.php │ ├── RecurringInvoice.php │ ├── Refund.php │ ├── SalesOrder.php │ ├── Settings │ │ ├── AutoReminder.php │ │ ├── Currency.php │ │ ├── Currency │ │ │ └── ExchangeRate.php │ │ ├── ManualReminder.php │ │ ├── OpeningBalance.php │ │ ├── Tax.php │ │ ├── TaxAuthority.php │ │ ├── TaxExemption.php │ │ └── TaxGroup.php │ ├── SubModel.php │ ├── Template.php │ ├── Unit.php │ ├── User.php │ ├── VendorCredit.php │ ├── VendorCredit │ │ ├── Bill.php │ │ └── Refund.php │ ├── VendorPayment.php │ └── VendorPayment │ │ └── Refund.php ├── Modules │ ├── BankAccounts.php │ ├── BankRules.php │ ├── BankTransactions.php │ ├── BaseCurrencyAdjustment.php │ ├── Bills.php │ ├── ChartOfAccounts.php │ ├── ChartOfAccounts │ │ └── Transactions.php │ ├── Contacts.php │ ├── Contacts │ │ ├── Addresses.php │ │ └── ContactPersons.php │ ├── CreditNotes.php │ ├── CustomerPayments.php │ ├── CustomerPayments │ │ └── Refunds.php │ ├── Documents.php │ ├── Estimates.php │ ├── Expenses.php │ ├── Import.php │ ├── Invoices.php │ ├── Items.php │ ├── Journals.php │ ├── Mixins │ │ ├── Commentable.php │ │ ├── Creditable.php │ │ ├── Payable.php │ │ └── Refundable.php │ ├── Module.php │ ├── Organizations.php │ ├── PreferenceModule.php │ ├── Projects.php │ ├── Projects │ │ ├── Tasks.php │ │ ├── TimeEntries.php │ │ └── Users.php │ ├── PurchaseOrders.php │ ├── RecurringExpenses.php │ ├── RecurringInvoices.php │ ├── SalesOrders.php │ ├── Settings.php │ ├── Settings │ │ ├── AutoReminders.php │ │ ├── CreditNotes.php │ │ ├── CreditNotes │ │ │ └── NotesAndTerms.php │ │ ├── Currencies.php │ │ ├── Currencies │ │ │ └── ExchangeRates.php │ │ ├── Estimates.php │ │ ├── Estimates │ │ │ └── NotesAndTerms.php │ │ ├── Invoices.php │ │ ├── Invoices │ │ │ └── NotesAndTerms.php │ │ ├── ManualReminders.php │ │ ├── OpeningBalances.php │ │ ├── Preferences.php │ │ ├── TaxAuthorities.php │ │ ├── TaxExemptions.php │ │ ├── TaxGroups.php │ │ └── Taxes.php │ ├── SubModule.php │ ├── Users.php │ ├── VendorCredits.php │ ├── VendorPayments.php │ └── VendorPayments │ │ └── Refunds.php ├── Request │ └── Pagination.php └── ZohoBooks.php └── tests ├── ApiTest.php ├── ClassNameGeneratorTest.php ├── TestCase.php └── config.example.json /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | vendor 3 | docs 4 | test.php 5 | .DS_Store 6 | tests/config.json 7 | composer.lock 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/webleit/zohobooksapi.svg?style=flat-square)](https://packagist.org/packages/webleit/zohobooksapi) 2 | [![Total Downloads](https://img.shields.io/packagist/dt/webleit/zohobooksapi.svg?style=flat-square)](https://packagist.org/packages/webleit/zohobooksapi) 3 | 4 | # Zoho Books API v3 - PHP SDK 5 | 6 | This Library is a SDK in PHP that simplifies the usage of the Zoho Books Api version 3 (https://www.zoho.com/books/api/v3/) 7 | It provides both an interface to ease the interaction with the APIs without bothering with the actual REST request, while packaging the various responses using very simple Model classes that can be then uses with any other library or framework. 8 | 9 | ## Installation 10 | 11 | ``` 12 | composer require webleit/zohobooksapi 13 | ``` 14 | 15 | ## Usage 16 | 17 | In order to use the library, just require the composer autoload file, and then fire up the library itself. 18 | In order for the library to work, you need to be authenticated with the zoho apis. 19 | 20 | ## Online Mode 21 | 22 | ```php 23 | require './vendor/autoload.php'; 24 | 25 | // setup the generic zoho oath client 26 | $oAuthClient = new \Weble\ZohoClient\OAuthClient('[CLIENT_ID]', '[CLIENT_SECRET]'); 27 | $oAuthClient->setRefreshToken('[REFRESH_TOKEN]'); 28 | $oAuthClient->setRegion(\Weble\ZohoClient\Enums\Region::us()); 29 | $oAuthClient->useCache($yourPSR6CachePool); 30 | 31 | // setup the zoho books client 32 | $client = new \Webleit\ZohoBooksApi\Client($oAuthClient); 33 | $client->setOrganizationId('[YOUR_ORGANIZATION_ID]'); 34 | 35 | // Create the main class 36 | $zohoBooks = new \Webleit\ZohoBooksApi\ZohoBooks($client); 37 | ``` 38 | 39 | ## Offline Mode 40 | 41 | This one is preferred when you need to autonomously renew the access token yourself. Used in all the "machine to machine" communication, and it's the best way when you are using the apis to, for example, sync with a 3rd party application, like your ERP or Ecommerce website. See https://github.com/Weble/ZohoClient#example-usage-offline-mode for more details on this. 42 | 43 | ```php 44 | require './vendor/autoload.php'; 45 | 46 | // setup the generic zoho oath client 47 | $oAuthClient = new \Weble\ZohoClient\OAuthClient('[CLIENT_ID]', '[CLIENT_SECRET]'); 48 | $oAuthClient->setRefreshToken('[REFRESH_TOKEN]'); 49 | $oAuthClient->setRegion(\Weble\ZohoClient\Enums\Region::us()); 50 | $oAuthClient->useCache($yourPSR6CachePool); 51 | $oAuthClient->offlineMode(); 52 | 53 | // Access Token 54 | $accessToken = $oAuthClient->getAccessToken(); 55 | $isExpired = $oAuthClient->accessTokenExpired(); 56 | 57 | // setup the zoho books client 58 | $client = new \Webleit\ZohoBooksApi\Client($oAuthClient); 59 | $client->setOrganizationId('[YOUR_ORGANIZATION_ID]'); 60 | 61 | // Create the main class 62 | $zohoBooks = new \Webleit\ZohoBooksApi\ZohoBooks($client); 63 | ``` 64 | 65 | ## API calls 66 | 67 | To call any Api, just use the same name reported in the api docs. 68 | You can get the list of supported apis using the getAvailableModules() method 69 | 70 | ```php 71 | $zohoBooks->getAvailableModules(); 72 | ``` 73 | 74 | You can, for example, get the list of invoices by using: 75 | 76 | ```php 77 | $invoices = $zohoBooks->invoices->getList(); 78 | ``` 79 | 80 | or the list of contacts 81 | 82 | ```php 83 | $contacts = $zohoBooks->contacts->getList(); 84 | ``` 85 | 86 | ### List calls 87 | 88 | To get a list of resources from a module, use the getList() method 89 | 90 | ```php 91 | $invoices = $zohoBooks->invoices->getList(); 92 | ``` 93 | 94 | It's possible to pass through some parameters to filter the result (see the zoho books api docs for some examples) 95 | 96 | ```php 97 | $invoices = $zohoBooks->invoices->getList(['status' => 'unpaid']); 98 | ``` 99 | 100 | In order to navigate the pages, just use the "page" and "per_page" parameters in the getList call 101 | 102 | ```php 103 | $invoices = $zohoBooks->invoices->getList(['status' => 'unpaid', 'page' => 3, 'per_page' => 200]); 104 | ``` 105 | 106 | 107 | ## Return Types 108 | 109 | Any "list" api call returns a Collection object, which is taken for Laravel Collection package. 110 | You can therefore use the result as Collection, which allows mapping, reducing, serializing, etc 111 | 112 | ```php 113 | $invoices = $zohoBooks->invoices->getList(); 114 | 115 | $data = $invoices->toArray(); 116 | $json = $invoices->toJson(); 117 | 118 | // After fetch filtering in php 119 | $filtered = $invoices->where('total', '>', 200); 120 | 121 | // Grouping 122 | $filtered = $invoices->groupBy('customer_id'); 123 | 124 | ``` 125 | 126 | Any "resource" api call returns a Model object of a class dedicated to the single resource you're fetching. 127 | For example, calling 128 | 129 | ```php 130 | $invoice = $zohoBooks->invoices->get('idoftheinvoice'); 131 | $id = $invoice->getId(); 132 | $data = $invoice->toArray(); 133 | $total = $invoice->total; 134 | 135 | ``` 136 | 137 | will return a \Webleit\ZohoBooksApi\Models\Invoice object, which is Arrayable and Jsonable, and that can be therefore used in many ways. 138 | 139 | ## Zoho Books token expiration notes 140 | * Each access token is valid for only an hour and used only for the operations defined in the scope. 141 | * Refresh token does not expire. Use it to refresh access tokens when they expire. 142 | * You can only generate a maximum of five refresh tokens in a minute. 143 | 144 | ## Contributing 145 | 146 | Finding bugs, sending pull requests or improving the docs - any contribution is welcome and highly appreciated 147 | 148 | ## Versioning 149 | 150 | Semantic Versioning Specification (SemVer) is used. 151 | 152 | ## Copyright and License 153 | 154 | Copyright Weble Srl under the MIT license. 155 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webleit/zohobooksapi", 3 | "description": "Zoho Books API v3 - PHP SDK", 4 | "license": "MIT", 5 | "keywords": ["zoho", "books", "zohobooks", "api"], 6 | "authors": [ 7 | { 8 | "name": "Daniele Rosario", 9 | "email": "daniele@weble.it" 10 | } 11 | ], 12 | "require": { 13 | "php" : "^7.2 || ^8.0", 14 | "guzzlehttp/guzzle": "^6.0 || ^7.0", 15 | "doctrine/inflector": "^2.0", 16 | "illuminate/collections": "^v8.0 || ^9.0 || ^10.0 || ^11.0 || ^12.0", 17 | "weble/zohoclient": "^4.2" 18 | }, 19 | "require-dev": { 20 | "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0 || ^10.0 || ^11.0", 21 | "cache/filesystem-adapter": "^1.1" 22 | }, 23 | "autoload": { 24 | "psr-4": { 25 | "Webleit\\ZohoBooksApi\\": "src" 26 | } 27 | }, 28 | "autoload-dev": { 29 | "psr-4": { 30 | "Webleit\\ZohoBooksApi\\Tests\\": "tests" 31 | } 32 | }, 33 | "minimum-stability": "dev", 34 | "prefer-stable": true 35 | } 36 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | tests 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/Client.php: -------------------------------------------------------------------------------- 1 | 'https://www.zohoapis.com/books/v3/', 26 | Region::AU => 'https://www.zohoapis.com.au/books/v3/', 27 | Region::EU => 'https://www.zohoapis.eu/books/v3/', 28 | Region::IN => 'https://www.zohoapis.in/books/v3/', 29 | Region::CN => 'https://www.zohoapis.com.cn/books/v3/', 30 | Region::JP => 'https://www.zohoapis.jp/books/v3/', 31 | ]; 32 | 33 | /** 34 | * @var bool 35 | */ 36 | protected $retriedRefresh = false; 37 | 38 | /** 39 | * Whilst this is technically a ClientInterface, we'll just mark it as a standard 40 | * client, with all the methods in place. 41 | * 42 | * @var \GuzzleHttp\Client 43 | */ 44 | protected $httpClient; 45 | 46 | /** 47 | * @var OAuthClient 48 | */ 49 | protected $oAuthClient; 50 | 51 | /** 52 | * default organization id 53 | * @var string 54 | */ 55 | protected $organizationId; 56 | 57 | /** 58 | * As of Zoho BUILD_VERSION "Dec_10_2019_23492", they are returning headers 59 | * of 'X-Rate-Limit-Limit', 'X-Rate-Limit-Reset', and 'X-Rate-Limit-Remaining' 60 | * on API calls. These vars are updated with the contents of those headers, if 61 | * they exist. 62 | */ 63 | 64 | /** 65 | * The rate limit of this org, as returned by the X-Rate-Limit-Limit header 66 | * @var int|null 67 | */ 68 | protected $orgRateLimit; 69 | 70 | /** 71 | * The number of seconds remaining until the rate limit resets, as returned 72 | * by the 'X-Rate-Limit-Reset' header 73 | * @var int|null 74 | */ 75 | protected $rateLimitReset; 76 | 77 | /** 78 | * The number of API calls remaining before the rate limit is reset, as returned 79 | * by the 'X-Rate-Limit-Remaining' header 80 | * @var int|null 81 | */ 82 | protected $rateLimitRemaining; 83 | 84 | public function __construct(OAuthClient $oAuthClient, ClientInterface $client = null) 85 | { 86 | if (!$client) { 87 | $client = new \GuzzleHttp\Client(); 88 | } 89 | 90 | $this->httpClient = $client; 91 | $this->oAuthClient = $oAuthClient; 92 | } 93 | 94 | public function __call($name, $arguments) 95 | { 96 | if (method_exists($this->oAuthClient, $name)) { 97 | return call_user_func_array([ 98 | $this->oAuthClient, 99 | $name 100 | ], $arguments); 101 | } 102 | } 103 | 104 | public function setRegion(string $region): self 105 | { 106 | $this->oAuthClient->setRegion($region); 107 | return $this; 108 | } 109 | 110 | public function getRegion(): string 111 | { 112 | return $this->oAuthClient->getRegion(); 113 | } 114 | 115 | public function getUrl(): string 116 | { 117 | return $this->regionDomain[$this->getRegion()] ?? $this->regionDomain[Region::US]; 118 | } 119 | 120 | public function getOrganizationId(): ?string 121 | { 122 | return $this->organizationId; 123 | } 124 | 125 | public function setOrganizationId(string $organizationId): self 126 | { 127 | $this->organizationId = $organizationId; 128 | return $this; 129 | } 130 | 131 | public function getList(string $url, array $filters = []): array 132 | { 133 | return $this->call($url, 'GET', [], ['query' => $filters]); 134 | } 135 | 136 | public function get(string $url, ?string $id = null, array $params = []) 137 | { 138 | if ($id !== null) { 139 | $url .= '/' . $id; 140 | } 141 | 142 | return $this->call( 143 | $url, 144 | 'GET', 145 | [], 146 | $params 147 | ); 148 | } 149 | 150 | /** 151 | * @throws ErrorResponseException 152 | */ 153 | public function rawGet(string $url, ?array $params = []): StreamInterface 154 | { 155 | try { 156 | $response = $this->httpClient->get($url, $this->getHttpClientOptions([], $params)); 157 | return $response->getBody(); 158 | } catch (\InvalidArgumentException $e) { 159 | throw new ErrorResponseException('Response from Zoho is not success. Message: ' . $e); 160 | } 161 | } 162 | 163 | public function post(string $url, array $data = [], array $params = []) 164 | { 165 | return $this->call( 166 | $url, 167 | 'POST', 168 | $data, 169 | $params 170 | ); 171 | } 172 | 173 | public function put(string $url, ?string $id = null, array $data = [], array $params = []) 174 | { 175 | if ($id !== null) { 176 | $url .= '/' . $id; 177 | } 178 | 179 | return $this->call($url, 'PUT', $data, $params); 180 | } 181 | 182 | public function delete(string $url, ?string $id = null) 183 | { 184 | if ($id !== null) { 185 | $url .= '/' . $id; 186 | } 187 | 188 | return $this->call($url, 'DELETE'); 189 | } 190 | 191 | public function call(string $uri, string $method, array $data = [], array $rawData = []) 192 | { 193 | $method = strtolower($method); 194 | try { 195 | $options = $this->getHttpClientOptions($data, $rawData); 196 | 197 | return $this->processResult( 198 | $this->httpClient->$method($this->getUrl() . $uri, $options) 199 | ); 200 | } catch (ClientException $e) { 201 | // Retry? 202 | if ($e->getCode() === 401 && ! $this->retriedRefresh) { 203 | $this->oAuthClient->refreshAccessToken(); 204 | $this->retriedRefresh = true; 205 | 206 | return $this->call($uri, $method, $data, $rawData); 207 | } 208 | 209 | throw $e; 210 | } 211 | } 212 | 213 | protected function getHttpClientOptions(array $data = [], array $rawData = []): array 214 | { 215 | $json = ['JSONString' => json_encode($data)]; 216 | 217 | return array_merge_recursive([ 218 | 'query' => [ 219 | 'organization_id' => $this->getOrganizationId() 220 | ], 221 | 'form_params' => $json, 222 | 'headers' => ['Authorization' => 'Zoho-oauthtoken ' . $this->oAuthClient->getAccessToken()] 223 | ], $rawData); 224 | } 225 | 226 | /** 227 | * @return array|string 228 | * @throws ErrorResponseException 229 | */ 230 | protected function processResult(ResponseInterface $response) 231 | { 232 | // Update the API Limit variables if they have been returned. 233 | $this->orgRateLimit = (int)$response->getHeaderLine('X-Rate-Limit-Limit'); 234 | $this->rateLimitRemaining = (int)$response->getHeaderline('X-Rate-Limit-Remaining'); 235 | $this->rateLimitReset = (int)$response->getHeaderLine('X-Rate-Limit-Reset'); 236 | 237 | try { 238 | $result = json_decode($response->getBody(), true); 239 | } catch (\InvalidArgumentException $e) { 240 | 241 | // All ok, probably not json, like PDF? 242 | if ($response->getStatusCode() >= 200 && $response->getStatusCode() <= 299) { 243 | return (string)$response->getBody(); 244 | } 245 | 246 | throw new ErrorResponseException($response->getReasonPhrase(), $response->getStatusCode()); 247 | } 248 | 249 | if (!$result) { 250 | // All ok, probably not json, like PDF? 251 | if ($response->getStatusCode() >= 200 && $response->getStatusCode() <= 299) { 252 | return (string)$response->getBody(); 253 | } 254 | 255 | throw new ErrorResponseException($response->getReasonPhrase(), $response->getStatusCode()); 256 | } 257 | 258 | if (isset($result['code']) && 0 == $result['code']) { 259 | return $result; 260 | } 261 | 262 | throw new ErrorResponseException('Response from Zoho is not success. Message: ' . $result['message'], $result['code'] ?? $response->getStatusCode()); 263 | } 264 | 265 | /** 266 | * Return the rate limits for this org. 267 | * 268 | * These values are taken from the headers provided by Zoho 269 | * as of BUILD_VERSION "Dec_10_2019_23492". If these values 270 | * are not provided, or are invalid, they will be null 271 | */ 272 | 273 | /** 274 | * @return int|null 275 | */ 276 | public function getOrgRateLimit() 277 | { 278 | return $this->orgRateLimit; 279 | } 280 | 281 | /** 282 | * @return int|null 283 | */ 284 | public function getRateLimitReset() 285 | { 286 | return $this->rateLimitReset; 287 | } 288 | 289 | /** 290 | * @return int|null 291 | */ 292 | public function getRateLimitRemaining() 293 | { 294 | return $this->rateLimitRemaining; 295 | } 296 | 297 | public function getHttpClient(): ClientInterface 298 | { 299 | return $this->httpClient; 300 | } 301 | } 302 | -------------------------------------------------------------------------------- /src/Contracts/Module.php: -------------------------------------------------------------------------------- 1 | availableModules))) { 15 | $class = $this->availableModules[$name]; 16 | return new $class($this->client); 17 | } 18 | } 19 | 20 | /** 21 | * Get the list of available modules 22 | * @return array 23 | */ 24 | public function getAvailableModules() 25 | { 26 | return $this->availableModules; 27 | } 28 | 29 | /** 30 | * Get the list of available modules keys 31 | * @return array 32 | */ 33 | public function getAvailableModuleKeys() 34 | { 35 | return array_keys($this->getAvailableModules()); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Models/BankAccount.php: -------------------------------------------------------------------------------- 1 | module->get($this->getId(), [ 25 | 'query' => ['accept' => 'pdf'], 26 | ]); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Models/Estimate.php: -------------------------------------------------------------------------------- 1 | data = $data ?? []; 33 | $this->module = $module; 34 | $this->inflector = InflectorFactory::create()->build(); 35 | } 36 | 37 | /** 38 | * @return Module 39 | */ 40 | public function getModule() 41 | { 42 | return $this->module; 43 | } 44 | 45 | /** 46 | * @param $name 47 | * @return mixed 48 | */ 49 | function __get($name) 50 | { 51 | if (isset($this->data[$name])) { 52 | return $this->data[$name]; 53 | } 54 | } 55 | 56 | /** 57 | * @param $name 58 | * @param $value 59 | */ 60 | function __set($name, $value) 61 | { 62 | $this->data[$name] = $value; 63 | } 64 | 65 | /** 66 | * @param $name 67 | */ 68 | function __isset($name) 69 | { 70 | return isset($this->data[$name]); 71 | } 72 | 73 | /** 74 | * @param $name 75 | * @param $arguments 76 | * @return mixed 77 | */ 78 | function __call($name, $arguments) 79 | { 80 | // add "id" as a parameter 81 | array_unshift($arguments, $this->getId()); 82 | 83 | if (method_exists($this->module, $name)) { 84 | return call_user_func_array([$this->module, $name], $arguments); 85 | } 86 | } 87 | 88 | /** 89 | * @return array 90 | */ 91 | public function getData() 92 | { 93 | return $this->data; 94 | } 95 | 96 | /** 97 | * @return array 98 | */ 99 | public function toArray() 100 | { 101 | return $this->getData(); 102 | } 103 | 104 | /** 105 | * @return string 106 | */ 107 | public function toJson() 108 | { 109 | return json_encode($this->toArray()); 110 | } 111 | 112 | /** 113 | * Convert the object into something JSON serializable. 114 | * 115 | * @return array 116 | */ 117 | #[\ReturnTypeWillChange] 118 | public function jsonSerialize() 119 | { 120 | return $this->toArray(); 121 | } 122 | 123 | /** 124 | * is a new object? 125 | * @return bool 126 | */ 127 | public function isNew() 128 | { 129 | return !$this->getId(); 130 | } 131 | 132 | /** 133 | * Get the id of the object 134 | * @return bool|string 135 | */ 136 | public function getId() 137 | { 138 | $key = $this->getKeyName(); 139 | return $this->$key ? $this->$key : false; 140 | } 141 | 142 | /** 143 | * Get the name of the primary key 144 | */ 145 | public function getKeyName() 146 | { 147 | if (method_exists($this->module, 'getApiKeyName')) { 148 | return $this->module->getApiKeyName(); 149 | } 150 | return strtolower($this->getName() . '_id'); 151 | } 152 | 153 | /** 154 | * @return string 155 | */ 156 | public function getName() 157 | { 158 | return $this->inflector->singularize(strtolower((new \ReflectionClass($this))->getShortName())); 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/Models/Organization.php: -------------------------------------------------------------------------------- 1 | pagination = $pagination; 22 | 23 | return $this; 24 | } 25 | 26 | public function pagination(): Pagination 27 | { 28 | return $this->pagination; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Models/RecurringExpense.php: -------------------------------------------------------------------------------- 1 | parent = $parent; 15 | } 16 | 17 | /** 18 | * @return Model 19 | */ 20 | public function getParent() 21 | { 22 | return $this->parent; 23 | } 24 | } -------------------------------------------------------------------------------- /src/Models/Template.php: -------------------------------------------------------------------------------- 1 | client->get($this->getUrl() . '/' . $id . '/statement/lastimported'); 21 | return new Statement($data['statement']); 22 | } 23 | 24 | /** 25 | * @param $id 26 | * @param $idOrStatement 27 | * @return bool 28 | */ 29 | public function deleteStatement($id, $idOrStatement) 30 | { 31 | if ($idOrStatement instanceof BankAccount\Statement) { 32 | $idOrStatement = $idOrStatement->getId(); 33 | } 34 | 35 | $this->client->delete($this->getUrl() . '/' . $id . '/statement/' . $idOrStatement); 36 | // If we arrive here without exceptions, everything went well 37 | return true; 38 | } 39 | 40 | /** 41 | * @param $id 42 | * @return bool 43 | */ 44 | public function deactivate($id) 45 | { 46 | return $this->doAction($id, 'inactice'); 47 | } 48 | 49 | /** 50 | * @param $id 51 | * @return bool 52 | */ 53 | public function activate($id) 54 | { 55 | return $this->doAction($id, 'active'); 56 | } 57 | } -------------------------------------------------------------------------------- /src/Modules/BankRules.php: -------------------------------------------------------------------------------- 1 | client->getList($this->getUrl() . '/uncategorized/' . $id . '/match', [], $params); 22 | 23 | $collection = new Collection($data['matching_transactions']); 24 | $collection = $collection->mapWithKeys(function ($item) { 25 | /** @var Module $item */ 26 | $item = new BankTransaction($item, $this); 27 | return [$item->getId() => $item]; 28 | }); 29 | 30 | return $collection; 31 | } 32 | 33 | /** 34 | * @param $id 35 | * @param array $params 36 | * @return BankTransaction 37 | */ 38 | public function getAssociated($id, $params = []) 39 | { 40 | $data = $this->client->get($this->getUrl() . '/' . $id . '/associated', null, $params); 41 | return new BankTransaction($data['transaction']); 42 | } 43 | 44 | /** 45 | * @param $id 46 | * @param array $params 47 | * @return bool 48 | */ 49 | public function exclude($id, $params = []) 50 | { 51 | $this->client->post($this->getUrl() . '/uncategorized/' . $id . '/exclude', [], $params); 52 | return true; 53 | } 54 | 55 | /** 56 | * @param $id 57 | * @param array $params 58 | * @return bool 59 | */ 60 | public function restore($id, $params = []) 61 | { 62 | $this->client->post($this->getUrl() . '/uncategorized/' . $id . '/restore', [], $params); 63 | return true; 64 | } 65 | 66 | /** 67 | * @param $id 68 | * @param $category 69 | * @param array $data 70 | * @param array $params 71 | * @return bool 72 | */ 73 | public function categorizeAs($id, $category, $data = [], $params = []) 74 | { 75 | $this->client->post($this->getUrl() . '/uncategorized/' . $id . '/categorize/' . $category, $data, $params); 76 | return true; 77 | } 78 | 79 | /** 80 | * @param $id 81 | * @param array $params 82 | * @return bool 83 | */ 84 | public function uncategorize($id, $params = []) 85 | { 86 | $this->client->post($this->getUrl() . '/uncategorized/' . $id . '/uncategorize/', [], $params); 87 | return true; 88 | } 89 | 90 | /** 91 | * @param $id 92 | * @param $data 93 | * @param array $params 94 | * @return array 95 | */ 96 | public function match($id, $data, $params = []) 97 | { 98 | $data = $this->client->post($this->getUrl() . '/uncategorized/' . $id . '/match', $data, $params); 99 | return $data['transactions_to_be_matched']; 100 | } 101 | 102 | /** 103 | * @param $id 104 | * @return bool 105 | */ 106 | public function unmatch($id) 107 | { 108 | $this->client->post($this->getUrl() . '/' . $id . '/unmatch'); 109 | // If we arrive here without exceptions, everything went well 110 | return true; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/Modules/BaseCurrencyAdjustment.php: -------------------------------------------------------------------------------- 1 | markAs($id, 'open'); 24 | } 25 | 26 | /** 27 | * @param $id string 28 | * @return boolean 29 | */ 30 | public function markAsVoid($id) 31 | { 32 | return $this->markAs($id, 'void'); 33 | } 34 | 35 | /** 36 | * @param $id 37 | * @param array $data 38 | * @return boolean 39 | */ 40 | public function updateBillingAddress($id, $data = []) 41 | { 42 | $this->client->put($this->getUrl() . '/' . $id . '/address/billing', null, $data); 43 | // If we arrive here without exceptions, everything went well 44 | return true; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Modules/ChartOfAccounts.php: -------------------------------------------------------------------------------- 1 | markAs('active', $id); 36 | } 37 | 38 | /** 39 | * @param $id 40 | * @return bool 41 | */ 42 | public function markAsInactive($id) 43 | { 44 | return $this->markAs('inactive', $id); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Modules/ChartOfAccounts/Transactions.php: -------------------------------------------------------------------------------- 1 | contactpersons = new ContactPersons($client); 37 | } 38 | 39 | /** 40 | * @param $id 41 | * @return \Illuminate\Support\Collection 42 | */ 43 | public function getContactPersons($id) 44 | { 45 | $className = $this->getModelClassName() . '\\Person'; 46 | return $this->getPropertyList('contactpersons', $id, $className, 'contact_persons', $this->contactpersons); 47 | } 48 | 49 | /** 50 | * @param $id 51 | * @return \Illuminate\Support\Collection 52 | */ 53 | public function getAddresses($id) 54 | { 55 | $className = $this->getModelClassName() . '\\Address'; 56 | return $this->getPropertyList('address', $id, $className, 'addresses', $this->addresses); 57 | } 58 | 59 | /** 60 | * @param string $id 61 | * @param array $from 62 | * @param array $to 63 | * @param array $data 64 | * @param string $filterBy - Status.All or Status.Outstanding 65 | * @param bool $sendUnpaidInvoiceList 66 | * @return bool 67 | */ 68 | public function emailStatement( 69 | $id, 70 | $from = null, 71 | $to = null, 72 | $data = [], 73 | $filterBy = 'Status.All', 74 | $sendUnpaidInvoiceList = false 75 | ) { 76 | $params = []; 77 | 78 | if ($from && !($from instanceof \DateTime)) { 79 | $from = new \DateTime($from); 80 | $params['start_date'] = $from->format('Y-m-d'); 81 | } 82 | 83 | if ($to && !($to instanceof \DateTime)) { 84 | $to = new \DateTime($to); 85 | $params['end_date'] = $to->format('Y-m-d'); 86 | } 87 | 88 | if (in_array($filterBy, ['Status.Outstanding', 'Status.All'])) { 89 | $params['filter_by'] = $filterBy; 90 | } 91 | 92 | //This apparently needs to be a string for zoho to accept it, so lets take a bool and make it a string 93 | $params['send_unpaid_invoice_list'] = $sendUnpaidInvoiceList 94 | ? 'true' 95 | : 'false'; 96 | 97 | $this->client->post( 98 | $this->getUrl() . '/' . $id . '/statements/email', 99 | $data, 100 | ['query' => $params], 101 | ); 102 | // If we arrive here without exceptions, everything went well 103 | return true; 104 | } 105 | 106 | /** 107 | * @param $id 108 | * @return array 109 | */ 110 | public function getStatementEmailContent($id, $from = null, $to = null) 111 | { 112 | $params = []; 113 | 114 | if ($from && !($from instanceof \DateTime)) { 115 | $from = new \DateTime($from); 116 | $params['start_date'] = $from->format('Y-m-d'); 117 | } 118 | 119 | if ($to && !($to instanceof \DateTime)) { 120 | $to = new \DateTime($to); 121 | $params['end_date'] = $to->format('Y-m-d'); 122 | } 123 | 124 | return $this->client->get($this->getUrl() . '/' . $id . '/statements/email', null, $params); 125 | } 126 | 127 | /** 128 | * @param $id 129 | * @param array $data 130 | * @param array $params 131 | * @return bool; 132 | */ 133 | public function email($id, $data = [], $params = []) 134 | { 135 | return $this->doAction($id, 'email', $data, $params); 136 | } 137 | 138 | /** 139 | * @param $id string 140 | * @return boolean 141 | */ 142 | public function markAsActive($id) 143 | { 144 | return $this->markAs($id, 'active'); 145 | } 146 | 147 | /** 148 | * @param $id string 149 | * @return boolean 150 | */ 151 | public function markAsInactive($id) 152 | { 153 | return $this->markAs($id, 'inactive'); 154 | } 155 | 156 | /** 157 | * @param $id string 158 | * @return boolean 159 | */ 160 | public function enablePaymentReminders($id) 161 | { 162 | return $this->markAs($id, 'enable', 'paymentreminders'); 163 | } 164 | 165 | /** 166 | * @param $id string 167 | * @return boolean 168 | */ 169 | public function disablePaymentReminders($id) 170 | { 171 | return $this->markAs($id, 'disable', 'paymentreminders'); 172 | } 173 | 174 | /** 175 | * @param $id string 176 | * @return boolean 177 | */ 178 | public function track1099($id) 179 | { 180 | return $this->doAction($id, 'track1099'); 181 | } 182 | 183 | /** 184 | * @param $id string 185 | * @return boolean 186 | */ 187 | public function untrack1099($id) 188 | { 189 | return $this->doAction($id, 'untrack1099'); 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /src/Modules/Contacts/Addresses.php: -------------------------------------------------------------------------------- 1 | client->post($this->getUrl() . '/' . $id . '/primary', ["random" => "data"]); 46 | 47 | // If we arrive here without exceptions, everything went well. 48 | return true; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Modules/CreditNotes.php: -------------------------------------------------------------------------------- 1 | getId(); 26 | } 27 | 28 | $this->client->delete($this->getUrl().'/'.$id.'/invoices/'.$idOrCreditNoteInvoice); 29 | // If we arrive here without exceptions, everything went well 30 | return true; 31 | } 32 | 33 | /** 34 | * @param $id 35 | * @param array $data Associative array of [$invoiceId => $amount] 36 | * @return bool 37 | */ 38 | public function applyToInvoices($id, array $data) 39 | { 40 | // This does not work, and the documentation is wrong 41 | // $data = $this->client->post($this->getUrl() . '/' . $id . '/invoices', null, $data); 42 | // return $data['apply_to_invoices']['invoices']; 43 | 44 | $result = true; 45 | foreach ($data as $invoiceId => $amount) { 46 | $result = $result && $this->applyToInvoice($id, $invoiceId, $amount); 47 | } 48 | 49 | return $result; 50 | } 51 | 52 | /** 53 | * Apply a credit note to an invoice 54 | * @param $id 55 | * @param $invoiceId 56 | * @param $amount 57 | * @return bool 58 | */ 59 | public function applyToInvoice($id, $invoiceId, $amount) 60 | { 61 | // This does not work, and the documentation is wrong 62 | // $data = $this->client->post($this->getUrl() . '/' . $id . '/invoices', null, $data); 63 | // return $data['apply_to_invoices']['invoices']; 64 | 65 | // let's use Invoices instead 66 | $invoices = new Invoices($this->getClient()); 67 | return $invoices->applyCreditNote($id, $invoiceId, $amount); 68 | } 69 | 70 | /** 71 | * @param $id 72 | * @return \Illuminate\Support\Collection 73 | */ 74 | public function getInvoices($id) 75 | { 76 | return $this->getPropertyList('invoices', $id, null, 'invoices_credited'); 77 | } 78 | 79 | /** 80 | * @param $id string 81 | * @return boolean 82 | */ 83 | public function markAsOpen($id) 84 | { 85 | return $this->markAs($id, 'open'); 86 | } 87 | 88 | /** 89 | * @param $id string 90 | * @return boolean 91 | */ 92 | public function markAsVoid($id) 93 | { 94 | return $this->markAs($id, 'void'); 95 | } 96 | 97 | /** 98 | * @param $id 99 | * @return array 100 | */ 101 | public function emailHistory($id) 102 | { 103 | return $this->client->get($this->getUrl().'/'.$id.'/emailhistory'); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/Modules/CustomerPayments.php: -------------------------------------------------------------------------------- 1 | refunds = new Refunds($client); 26 | } 27 | 28 | /** 29 | * @return string 30 | */ 31 | public function getName() 32 | { 33 | return 'payment'; 34 | } 35 | 36 | /** 37 | * @return string 38 | */ 39 | public function getResourceKey() 40 | { 41 | return 'customerpayments'; 42 | } 43 | 44 | /** 45 | * @return string 46 | */ 47 | public function getResourceItemKey() 48 | { 49 | return 'payment'; 50 | } 51 | 52 | /** 53 | * @param $id 54 | * @return \Illuminate\Support\Collection 55 | */ 56 | public function getRefunds($id) 57 | { 58 | $className = $this->getModelClassName() . '\\Refund'; 59 | return $this->getPropertyList('refunds', $id, $className, 'payment_refunds', $this->refunds); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Modules/CustomerPayments/Refunds.php: -------------------------------------------------------------------------------- 1 | client->get($this->getUrl() . '/' . $id . '/email', null, $params); 26 | } 27 | 28 | /** 29 | * @param string $id 30 | * @param array $data 31 | * @param array $params 32 | * @return bool 33 | */ 34 | public function email($id, $data = [], $params = []) 35 | { 36 | return $this->emailOne($id, $data, $params); 37 | } 38 | 39 | /** 40 | * @param array $ids 41 | * @param array $data 42 | * @return bool 43 | */ 44 | public function emailList($ids = [], $data = []) 45 | { 46 | $params['invoice_ids'] = implode(",", $ids); 47 | 48 | $this->client->post($this->getUrl() . '/email', $data, $params); 49 | // If we arrive here without exceptions, everything went well 50 | return true; 51 | } 52 | 53 | /** 54 | * @param $id 55 | * @param array $data 56 | * @return boolean 57 | */ 58 | public function updateBillingAddress($id, $data = []) 59 | { 60 | $this->client->put($this->getUrl() . '/' . $id . '/address/billing', null, $data); 61 | // If we arrive here without exceptions, everything went well 62 | return true; 63 | } 64 | 65 | /** 66 | * @param $id 67 | * @param array $data 68 | * @return boolean 69 | */ 70 | public function updateShippingAddress($id, $data = []) 71 | { 72 | $this->client->put($this->getUrl() . '/' . $id . '/address/shipping', null, $data); 73 | // If we arrive here without exceptions, everything went well 74 | return true; 75 | } 76 | 77 | /** 78 | * @param $id string 79 | * @return boolean 80 | */ 81 | public function markAsSent($id) 82 | { 83 | return $this->markAs($id, 'sent'); 84 | } 85 | 86 | /** 87 | * @param $id 88 | * @param $data 89 | * @param $params 90 | * @return bool 91 | */ 92 | public function emailOne($id, $data, $params) 93 | { 94 | return $this->doAction($id, 'email', $data, $params); 95 | } 96 | 97 | /** 98 | * @param array $ids 99 | * @return StreamInterface The content of the pdf as a stream 100 | */ 101 | public function exportPdfList($ids = []) 102 | { 103 | $params['query']['invoice_ids'] = implode(",", $ids); 104 | return $this->client->rawGet($this->client->getUrl() . $this->getUrl() . '/pdf', $params); 105 | } 106 | 107 | /** 108 | * @return Collection 109 | */ 110 | public function getTemplates() 111 | { 112 | return $this->getPropertyList('templates'); 113 | } 114 | 115 | /** 116 | * @param string $id 117 | * @param string|Template $idOrTemplate 118 | * @return bool 119 | */ 120 | public function updateTemplate($id, $idOrTemplate) 121 | { 122 | if ($idOrTemplate instanceof Template) { 123 | $idOrTemplate = $idOrTemplate->getId(); 124 | } 125 | 126 | $this->client->put($this->getUrl() . '/' . $id . '/templates/' . $idOrTemplate); 127 | // If we arrive here without exceptions, everything went well 128 | return true; 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/Modules/Estimates.php: -------------------------------------------------------------------------------- 1 | markAs($id, 'declined'); 18 | } 19 | 20 | /** 21 | * @param $id string 22 | * @return boolean 23 | */ 24 | public function markAsAccepted($id) 25 | { 26 | return $this->markAs($id, 'accepted'); 27 | } 28 | } -------------------------------------------------------------------------------- /src/Modules/Expenses.php: -------------------------------------------------------------------------------- 1 | client = $client; 24 | } 25 | 26 | /** 27 | * @return Client 28 | */ 29 | public function getClient() 30 | { 31 | return $this->client; 32 | } 33 | 34 | /** 35 | * @param $crm_contact_id 36 | * @return Contact 37 | * @throws ErrorResponseException 38 | */ 39 | public function contact($crm_contact_id): Contact 40 | { 41 | return $this->importFromCRM($crm_contact_id, 'contact'); 42 | } 43 | 44 | 45 | /** 46 | * @param $crm_account_id 47 | * @return Contact 48 | * @throws ErrorResponseException 49 | */ 50 | public function account($crm_account_id): Contact 51 | { 52 | return $this->importFromCRM($crm_account_id, 'account'); 53 | } 54 | 55 | 56 | /** 57 | * @param $crm_vendor_id 58 | * @return Contact 59 | * @throws ErrorResponseException 60 | */ 61 | public function vendor($crm_vendor_id): Contact 62 | { 63 | return $this->importFromCRM($crm_vendor_id, 'vendor'); 64 | } 65 | 66 | 67 | /** 68 | * @param $crm_id 69 | * @param $urlPath 70 | * @throws ErrorResponseException 71 | */ 72 | public function importFromCRM($crm_id, $urlPath) 73 | { 74 | try { 75 | $data = $this->client->post( 76 | 'crm/' . $urlPath . '/' . $crm_id . '/import', ['blank' => ''] 77 | ); 78 | } catch (ClientException $e) { 79 | throw new ErrorResponseException('Response from Zoho is not success. Message: ' . $e->getMessage(), $e->getCode() ?: 500); 80 | } 81 | 82 | if (isset($data['data']['customer_id'])) { 83 | return (new Contacts($this->getClient()))->get($data['data']['customer_id']); 84 | } 85 | 86 | throw new ErrorResponseException('Response from Zoho is not success. Message: ' . $data['message'], $data['code'] ?? 500); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/Modules/Invoices.php: -------------------------------------------------------------------------------- 1 | client->post($this->getUrl() . '/paymentreminder', [], $params); 29 | // If we arrive here without exceptions, everything went well 30 | return true; 31 | } 32 | 33 | /** 34 | * @param $id 35 | * @return bool 36 | */ 37 | public function disablePaymentReminder($id) 38 | { 39 | return $this->markAs($id, 'disable', 'paymentreminder'); 40 | } 41 | 42 | /** 43 | * @param $id 44 | * @return bool 45 | */ 46 | public function enablePaymentReminder($id) 47 | { 48 | return $this->markAs($id, 'enable', 'paymentreminder'); 49 | } 50 | 51 | /** 52 | * @param $id 53 | * @param $data 54 | * @param $params 55 | * @return bool 56 | */ 57 | public function sendPaymentReminder($id, $data = [], $params = []) 58 | { 59 | $this->client->post($this->getUrl() . '/' . $id . '/paymentreminder', $data, $params); 60 | // If we arrive here without exceptions, everything went well 61 | return true; 62 | } 63 | 64 | /** 65 | * @param $id 66 | * @return array 67 | */ 68 | public function getPaymentReminderEmailContent($id) 69 | { 70 | return $this->client->get($this->getUrl() . '/' . $id . '/paymentreminder'); 71 | } 72 | 73 | 74 | /** 75 | * @param $id 76 | * @return bool 77 | */ 78 | public function void($id) 79 | { 80 | $this->markAs($id, 'void'); 81 | // If we arrive here without exceptions, everything went well 82 | return true; 83 | } 84 | 85 | /** 86 | * @param $id 87 | * @return boolean 88 | */ 89 | public function writeOff($id) 90 | { 91 | return $this->doAction($id, 'writeoff'); 92 | } 93 | 94 | /** 95 | * @param $id 96 | * @return boolean 97 | */ 98 | public function cancelWriteOff($id) 99 | { 100 | return $this->doAction($id, 'writeoff/cancel'); 101 | } 102 | 103 | /** 104 | * @param $id string 105 | * @return boolean 106 | */ 107 | public function markAsDraft($id) 108 | { 109 | return $this->markAs($id, 'draft'); 110 | } 111 | 112 | /** 113 | * Apply a credit note to an invoice. 114 | * 115 | * @param string $invid Invoice ID 116 | * @param string $creditid Credit Note ID 117 | * @param float $amount Amount of credit note to apply. 118 | * 119 | * @return bool 120 | * @see Invoices::applyCreditNotes() 121 | * 122 | */ 123 | public function applyCreditNote($invid, $creditid, $amount) 124 | { 125 | return $this->applyCreditNotes($invid, [$creditid => $amount]); 126 | } 127 | 128 | /** 129 | * Apply multiple credit notes to an invoice. 130 | * 131 | * Note that is is NOT DOCUMENTED in their API, and the way they say 132 | * to do it does not work. I had to reverse engineer this by looking at 133 | * what the React web client uses in the developer console. 134 | * 135 | * @param string $invid Invoice ID 136 | * @param array $creditNotesAmounts Associative array of [$creditNoteId => $amountToApply] 137 | * 138 | * @return bool 139 | */ 140 | public function applyCreditNotes($invid, array $creditNotesAmounts) 141 | { 142 | $creditData = []; 143 | 144 | foreach ($creditNotesAmounts as $creditId => $amount) { 145 | $creditData[] = [ 146 | "creditnote_id" => $creditId, 147 | "amount_applied" => $amount 148 | ]; 149 | } 150 | 151 | $data = [ 152 | 'apply_creditnotes' => $creditData 153 | ]; 154 | 155 | $this->client->post($this->getUrl() . '/' . $invid . '/credits', $data); 156 | return true; 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/Modules/Items.php: -------------------------------------------------------------------------------- 1 | doAction($id, 'active'); 18 | } 19 | 20 | /** 21 | * @param $id 22 | * @return bool 23 | */ 24 | public function markAsInactive($id) 25 | { 26 | return $this->doAction($id, 'inactive'); 27 | } 28 | } -------------------------------------------------------------------------------- /src/Modules/Journals.php: -------------------------------------------------------------------------------- 1 | getUrl() . '/' . $id . '/comments'; 16 | $list = $this->client->getList($url); 17 | 18 | $prefix = $this->inflector->singularize(strtolower($this->getName())) . '_'; 19 | 20 | $collection = new Collection($list[$prefix . 'comments']); 21 | $collection = $collection->mapWithKeys(function ($item) { 22 | /** @var Model $item */ 23 | $item = new Comment($item, $this); 24 | return [$item->getId() => $item]; 25 | }); 26 | 27 | return $collection; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Modules/Mixins/Creditable.php: -------------------------------------------------------------------------------- 1 | getPropertyList('creditsapplied', $id, $this->creditsAppliedClass ?? null, 'credits'); 17 | } 18 | 19 | /** 20 | * @param $id 21 | * @param string|CreditsApplied $idOrCredit 22 | * @return bool 23 | */ 24 | public function deleteAppliedCredit($id, $idOrCredit) 25 | { 26 | if ($idOrCredit instanceof CreditsApplied) { 27 | $idOrCredit = $idOrCredit->getId(); 28 | } 29 | 30 | $this->client->delete($this->getUrl() . '/' . $id . '/creditsapplied/' . $idOrCredit); 31 | return true; 32 | } 33 | } -------------------------------------------------------------------------------- /src/Modules/Mixins/Payable.php: -------------------------------------------------------------------------------- 1 | getPropertyList('payments', $id); 17 | } 18 | 19 | /** 20 | * @param $id 21 | * @param array $data 22 | * @return bool 23 | */ 24 | public function applyCredits($id, $data = []) 25 | { 26 | $data = $this->client->post($this->getUrl() . '/' . $id . '/credits', $data); 27 | 28 | return $data['use_credits']; 29 | } 30 | 31 | /** 32 | * @param $id 33 | * @param string|Payment $idOrPayment 34 | * @return bool 35 | */ 36 | public function deletePayment($id, $idOrPayment) 37 | { 38 | if ($idOrPayment instanceof Payment) { 39 | $idOrPayment = $idOrPayment->getId(); 40 | } 41 | 42 | $this->client->delete($this->getUrl() . '/' . $id . '/payments/' . $idOrPayment); 43 | return true; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Modules/Mixins/Refundable.php: -------------------------------------------------------------------------------- 1 | getUrl() . '/' . $id . '/refunds'; 17 | $list = $this->client->getList($url); 18 | 19 | $prefix = Inflector::singularize(strtolower($this->getName())) . '_'; 20 | 21 | $collection = new Collection($list[$prefix . 'comments']); 22 | $collection = $collection->mapWithKeys(function ($item) { 23 | /** @var Model $item */ 24 | $item = new Refund($item, $this); 25 | return [$item->getId() => $item]; 26 | }); 27 | 28 | return $collection; 29 | } 30 | } -------------------------------------------------------------------------------- /src/Modules/Module.php: -------------------------------------------------------------------------------- 1 | client = $client; 40 | $this->inflector = InflectorFactory::create()->build(); 41 | } 42 | 43 | /** 44 | * Get the list of the resources requested 45 | * @param array $params 46 | * @return RecordCollection 47 | */ 48 | public function getList($params = []) 49 | { 50 | $list = $this->client->getList($this->getUrl(), $params); 51 | 52 | $collection = new RecordCollection($list[$this->getResourceKey()]); 53 | $collection = $collection->mapWithKeys(function ($item) { 54 | $item = $this->make($item); 55 | return [$item->getId() => $item]; 56 | }); 57 | 58 | $collection->withPagination(new Pagination($list['page_context'] ?? [])); 59 | 60 | return $collection; 61 | } 62 | 63 | /** 64 | * Get a single record for this module 65 | * @param string $id 66 | * @return Model|string 67 | */ 68 | public function get($id, array $params = []) 69 | { 70 | $item = $this->client->get($this->getUrl(), $id, $params); 71 | 72 | if (!is_array($item)) { 73 | return $item; 74 | } 75 | 76 | $data = $item[$this->inflector->singularize($this->getResourceItemKey())]; 77 | 78 | return $this->make($data); 79 | } 80 | 81 | /** 82 | * Get the total records for a module 83 | * @return int 84 | */ 85 | public function getTotal() 86 | { 87 | $list = $this->client->getList($this->getUrl(), ['response_option' => self::RESPONSE_OPTION_PAGINATION_ONLY]); 88 | return $list['page_context']['total']; 89 | } 90 | 91 | /** 92 | * Creates a new record for this module 93 | * @param array $data 94 | * @param array $params 95 | * @return Model 96 | */ 97 | public function create($data, $params = []) 98 | { 99 | $data = $this->client->post($this->getUrl(), $data, $params); 100 | $data = $data[$this->inflector->singularize($this->getResourceItemKey())]; 101 | 102 | return $this->make($data); 103 | } 104 | 105 | /** 106 | * Update a record for this module 107 | * @param string $id 108 | * @param array $data 109 | * @param array $params 110 | * @return Model 111 | */ 112 | public function update($id, $data, $params = []) 113 | { 114 | $data = $this->client->put($this->getUrl(), $id, $data, $params); 115 | $data = $data[$this->inflector->singularize($this->getResourceItemKey())]; 116 | 117 | return $this->make($data); 118 | } 119 | 120 | /** 121 | * Deletes a record for this module 122 | * 123 | * @param $id 124 | * @return bool 125 | */ 126 | public function delete($id) 127 | { 128 | $this->client->delete($this->getUrl(), $id); 129 | 130 | // all is ok if we've reached this point 131 | return true; 132 | } 133 | 134 | /** 135 | * Get the url path for the api of this module (ie: /organizations) 136 | * @return string 137 | */ 138 | public function getUrlPath() 139 | { 140 | // Module specific url path? 141 | if (isset($this->urlPath) && $this->urlPath) { 142 | return $this->urlPath; 143 | } 144 | 145 | // Class name 146 | return $this->getName(); 147 | } 148 | 149 | /** 150 | * @return string 151 | */ 152 | public function getName() 153 | { 154 | return $this->inflector->pluralize(strtolower((new \ReflectionClass($this))->getShortName())); 155 | } 156 | 157 | /** 158 | * Get the full api url to this module 159 | * @return string 160 | */ 161 | public function getUrl() 162 | { 163 | return $this->getUrlPath(); 164 | } 165 | 166 | /** 167 | * This is used to determine the key of the returned data 168 | * 169 | * Note that some modules (eg, Settings\TaxExemptions) override 170 | * this value, because zoho does not return the data with the 171 | * expected key. If you are looking at this code, you may also 172 | * need to override the api key name, too. 173 | * 174 | * @return string 175 | */ 176 | public function getResourceKey() 177 | { 178 | return strtolower($this->getName()); 179 | } 180 | 181 | /** 182 | * This is used to determine the key of the returned data in get() calls 183 | * 184 | * Note that some modules (eg, Settings\TaxExemptions) override 185 | * this value, because zoho does not return the data with the 186 | * expected key. If you are looking at this code, you may also 187 | * need to override the api key name, too. 188 | * 189 | * @return string 190 | */ 191 | public function getResourceItemKey() 192 | { 193 | return $this->getResourceKey(); 194 | } 195 | 196 | /** 197 | * @param array $data 198 | * @return Model 199 | */ 200 | public function make($data = []) 201 | { 202 | $class = $this->getModelClassName(); 203 | 204 | return new $class($data, $this); 205 | } 206 | 207 | /** 208 | * @return Client 209 | */ 210 | public function getClient() 211 | { 212 | return $this->client; 213 | } 214 | 215 | /** 216 | * Mark something with a status 217 | * 218 | * Note that as of 2019-11-27, Zoho Books API errors if you don't 219 | * provide a JSONString param. It can't be empty. 220 | * 221 | * See https://github.com/Weble/ZohoBooksApi/issues/33 222 | * 223 | * @param $id 224 | * @param $status 225 | * @param string $key 226 | * @return bool 227 | */ 228 | public function markAs($id, $status, $key = 'status') 229 | { 230 | $this->client->post( 231 | $this->getUrl() . '/' . $id . '/' . $key . '/' . $status, 232 | ["random" => "data"] 233 | ); 234 | // If we arrive here without exceptions, everything went well 235 | return true; 236 | } 237 | 238 | /** 239 | * @param $id 240 | * @param $action 241 | * @param array $data 242 | * @param array $params 243 | * @return bool 244 | */ 245 | public function doAction($id, $action, $data = [], $params = []) 246 | { 247 | $this->client->post($this->getUrl() . '/' . $id . '/' . $action, $data, $params); 248 | 249 | // If we arrive here without exceptions, everything went well 250 | return true; 251 | } 252 | 253 | /** 254 | * @param $property 255 | * @param null $id 256 | * @param null $class 257 | * @param null $subProperty 258 | * @param null $module 259 | * @return Collection 260 | */ 261 | protected function getPropertyList($property, $id = null, $class = null, $subProperty = null, $module = null) 262 | { 263 | if (!$class) { 264 | $class = $this->getModelClassName() . '\\' . ucfirst(strtolower($this->inflector->singularize($property))); 265 | } 266 | 267 | if (!$module) { 268 | $module = $this; 269 | } 270 | 271 | if (!$subProperty) { 272 | $subProperty = $property; 273 | } 274 | 275 | $url = $this->getUrl(); 276 | if ($id !== null) { 277 | $url .= '/' . $id; 278 | } 279 | $url .= '/' . $property; 280 | 281 | $list = $this->client->getList($url); 282 | 283 | $collection = new Collection($list[$subProperty]); 284 | $collection = $collection->mapWithKeys(function ($item) use ($class, $module) { 285 | /** @var Model $item */ 286 | $item = new $class($item, $module); 287 | return [$item->getId() => $item]; 288 | }); 289 | 290 | return $collection; 291 | } 292 | 293 | /** 294 | * @return string 295 | */ 296 | public function getModelClassName() 297 | { 298 | $className = (new \ReflectionClass($this))->getShortName(); 299 | $class = '\\Webleit\\ZohoBooksApi\\Models\\' . ucfirst($this->inflector->singularize($className)); 300 | 301 | return $class; 302 | } 303 | } 304 | -------------------------------------------------------------------------------- /src/Modules/Organizations.php: -------------------------------------------------------------------------------- 1 | getList(); 19 | 20 | foreach ($organizations as $organization) { 21 | if ($organization->is_default_org) { 22 | return $organization; 23 | } 24 | } 25 | 26 | return false; 27 | } 28 | 29 | /** 30 | * @return bool|string 31 | */ 32 | public function getDefaultOrganizationId() 33 | { 34 | $organization = $this->getDefaultOrganization(); 35 | 36 | return $organization ? $organization->getId() : false; 37 | } 38 | 39 | /** 40 | * @param $data 41 | * @return array 42 | */ 43 | public function addAddress($data) 44 | { 45 | $data = $this->client->post($this->getUrl() . '/address', $data); 46 | return $data['organization_address']; 47 | } 48 | 49 | /** 50 | * @param $id 51 | * @param $data 52 | * @return array 53 | */ 54 | public function updateAddress($id, $data) 55 | { 56 | $data = $this->client->put($this->getUrl() . '/address/' . $id, null, $data); 57 | return $data['organization_address']; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Modules/PreferenceModule.php: -------------------------------------------------------------------------------- 1 | client = $client; 36 | $this->inflector = InflectorFactory::create()->build(); 37 | } 38 | 39 | /** 40 | * Get the list of the resources requested 41 | * @param array $params 42 | * @return Collection 43 | */ 44 | public function getList($params = []) 45 | { 46 | $list = $this->client->getList($this->getUrl(), $params); 47 | 48 | $collection = new Collection($list[$this->getAnswerKeyName()]); 49 | return $collection; 50 | } 51 | 52 | /** 53 | * Update settings for this module 54 | * @param array $data 55 | * @return Collection 56 | */ 57 | public function update($data) 58 | { 59 | $data = $this->client->put($this->getUrl(), null, $data); 60 | $collection = new Collection($data[$this->getAnswerKeyName()]); 61 | 62 | return $collection; 63 | } 64 | 65 | /** 66 | * Get the url path for the api of this module (ie: /organizations) 67 | * @return string 68 | */ 69 | public function getUrlPath() 70 | { 71 | // Module specific url path? 72 | if (isset($this->urlPath) && $this->urlPath) { 73 | return $this->urlPath; 74 | } 75 | 76 | // Class name 77 | return 'settings/' . $this->getName(); 78 | } 79 | 80 | /** 81 | * @return string 82 | */ 83 | public function getName() 84 | { 85 | return $this->inflector->pluralize(strtolower((new \ReflectionClass($this))->getShortName())); 86 | } 87 | 88 | /** 89 | * @return string 90 | */ 91 | protected function getAnswerKeyName() 92 | { 93 | return $this->getName(); 94 | } 95 | 96 | /** 97 | * Get the full api url to this module 98 | * @return string 99 | */ 100 | public function getUrl() 101 | { 102 | return self::ENDPOINT . $this->getUrlPath(); 103 | } 104 | 105 | /** 106 | * @return Client 107 | */ 108 | public function getClient() 109 | { 110 | return $this->client; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/Modules/Projects.php: -------------------------------------------------------------------------------- 1 | client, $this->get($id)); 26 | $className = $tasks->getModelClassName(); 27 | 28 | return $this->getPropertyList('tasks', $id, $className, 'tasks', $tasks); 29 | } 30 | 31 | /** 32 | * @param $id 33 | * @return \Illuminate\Support\Collection 34 | */ 35 | public function getUsers($id) 36 | { 37 | $tasks = new Users($this->client, $this->get($id)); 38 | $className = $tasks->getModelClassName(); 39 | 40 | return $this->getPropertyList('users', $id, $className, 'users', $tasks); 41 | } 42 | 43 | /** 44 | * @param $id 45 | * @return \Illuminate\Support\Collection 46 | */ 47 | public function getTimeEntries($id) 48 | { 49 | $tasks = new TimeEntries($this->client); 50 | $className = $tasks->getModelClassName(); 51 | 52 | return $this->getPropertyList('timeentries', $id, $className, 'time_entries', $tasks); 53 | } 54 | 55 | /** 56 | * @param $id 57 | * @return \Illuminate\Support\Collection 58 | */ 59 | public function getInvoices($id) 60 | { 61 | return $this->getPropertyList('invoices', $id); 62 | } 63 | 64 | /** 65 | * @param $id 66 | */ 67 | public function activate($id) 68 | { 69 | $this->doAction($id, 'active'); 70 | } 71 | 72 | /** 73 | * @param $id 74 | */ 75 | public function inactivate($id) 76 | { 77 | $this->doAction($id, 'inactive'); 78 | } 79 | 80 | /** 81 | * @param $id 82 | */ 83 | public function deactivate($id) 84 | { 85 | $this->inactivate($id); 86 | } 87 | 88 | /** 89 | * @param $id 90 | * @param $name 91 | * @param string $description 92 | * @return Project 93 | */ 94 | public function cloneAs($id, $name, $description = '') 95 | { 96 | $data = [ 97 | 'project_name' => $name, 98 | 'description' => $description 99 | ]; 100 | 101 | $data = $this->client->post($this->getUrl() . '/' . $id . '/clone', $data); 102 | return new Project($this, $data); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/Modules/Projects/Tasks.php: -------------------------------------------------------------------------------- 1 | client->post($this->getUrl(), $data); 39 | return new Project\TimeEntry($data['time_entry'], $this); 40 | } 41 | 42 | /** 43 | * @param $ids 44 | * @return bool 45 | */ 46 | public function deleteList($ids) 47 | { 48 | $this->client->delete($this->getUrl(), null, ['time_entry_ids' => $ids]); 49 | return true; 50 | } 51 | 52 | /** 53 | * @return Project\TimeEntry 54 | */ 55 | public function getTimer() 56 | { 57 | $data = $this->client->get($this->getUrl() . '/runningtimer/me'); 58 | return new Project\TimeEntry($data['time_entry'], $this); 59 | } 60 | 61 | /** 62 | * @param $id 63 | * @return Project\TimeEntry 64 | */ 65 | public function startTime($id) 66 | { 67 | $data = $this->client->post($this->getUrl() . '/' . $id . '/timer/start'); 68 | return new Project\TimeEntry($data['time_entry'], $this); 69 | } 70 | 71 | /** 72 | * @param $id 73 | * @return Project\TimeEntry 74 | */ 75 | public function stopTimer($id) 76 | { 77 | $data = $this->client->post($this->getUrl() . '/' . $id . '/timer/stop'); 78 | return new Project\TimeEntry($data['time_entry'], $this); 79 | } 80 | 81 | /** 82 | * @param $id 83 | * @param $data 84 | * @return Project\TimeEntry 85 | */ 86 | public function update($id, $data, $params = []) 87 | { 88 | $data = $this->client->put($this->getUrl(), $id, $data); 89 | return new Project\TimeEntry($data['time_entry'], $this); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/Modules/Projects/Users.php: -------------------------------------------------------------------------------- 1 | client->post($this->getUrl(), $data); 23 | return $data['users']; 24 | } 25 | 26 | /** 27 | * @param $data 28 | * @return mixed 29 | */ 30 | public function invite($data) 31 | { 32 | $data = $this->client->post($this->getUrl() . '/invite', $data); 33 | return $data['user']; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Modules/PurchaseOrders.php: -------------------------------------------------------------------------------- 1 | markAs($id, 'open'); 18 | } 19 | 20 | /** 21 | * @param $id string 22 | * @return boolean 23 | */ 24 | public function markAsBilled($id) 25 | { 26 | return $this->markAs($id, 'billed'); 27 | } 28 | 29 | /** 30 | * @param $id string 31 | * @return boolean 32 | */ 33 | public function cancel($id) 34 | { 35 | return $this->markAs($id, 'cancelled'); 36 | } 37 | } -------------------------------------------------------------------------------- /src/Modules/RecurringExpenses.php: -------------------------------------------------------------------------------- 1 | markAs($id, 'stop'); 20 | } 21 | 22 | /** 23 | * @param $id string 24 | * @return boolean 25 | */ 26 | public function resume($id) 27 | { 28 | return $this->markAs($id, 'resume'); 29 | } 30 | 31 | /** 32 | * @param $id 33 | * @return Collection 34 | */ 35 | public function getChildExpenses($id) 36 | { 37 | return $this->getPropertyList('expenses', $id); 38 | } 39 | 40 | /** 41 | * Override returned key 42 | * 43 | * This overrides the key that is returned from zoho, as they 44 | * send back 'recurring_expenses' instead of 'recurringexpenses' 45 | * 46 | * @return string 47 | */ 48 | public function getResourceKey() 49 | { 50 | return 'recurring_expenses'; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Modules/RecurringInvoices.php: -------------------------------------------------------------------------------- 1 | markAs($id, 'stop'); 18 | } 19 | 20 | /** 21 | * @param $id string 22 | * @return boolean 23 | */ 24 | public function resume($id) 25 | { 26 | return $this->markAs($id, 'resume'); 27 | } 28 | 29 | /** 30 | * Override returned key 31 | * 32 | * This overrides the key that is returned from zoho, as they 33 | * send back 'recurring_invoices' instead of 'recurringinvoices' 34 | * 35 | * @return string 36 | */ 37 | public function getResourceKey() 38 | { 39 | return 'recurring_invoices'; 40 | } 41 | 42 | /** 43 | * Override returned API key name 44 | * 45 | * This overrides the API key name that is returned from zoho, as they 46 | * send back 'recurring_invoice_id' instead of 'recurringinvoices_id' 47 | * 48 | * @return string 49 | */ 50 | public function getApiKeyName() 51 | { 52 | return 'recurring_invoice_id'; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Modules/SalesOrders.php: -------------------------------------------------------------------------------- 1 | markAs($id, 'open'); 18 | } 19 | 20 | /** 21 | * @param $id string 22 | * @return boolean 23 | */ 24 | public function markAsVoid($id) 25 | { 26 | return $this->markAs($id, 'void'); 27 | } 28 | } -------------------------------------------------------------------------------- /src/Modules/Settings.php: -------------------------------------------------------------------------------- 1 | Settings\Invoices::class, 40 | 'preferences' => Settings\Preferences::class, 41 | 'estimates' => Settings\Estimates::class, 42 | 'creditnotes' => Settings\CreditNotes::class, 43 | 'currencies' => Settings\Currencies::class, 44 | 'taxes' => Settings\Taxes::class, 45 | 'taxgroups' => Settings\TaxGroups::class, 46 | 'taxauthorities' => Settings\TaxAuthorities::class, 47 | 'taxexemptions' => Settings\TaxExemptions::class, 48 | 'openingbalances' => Settings\OpeningBalances::class, 49 | 'autoreminders' => Settings\AutoReminders::class, 50 | 'manualreminders' => Settings\ManualReminders::class 51 | ]; 52 | 53 | /** 54 | * @var string 55 | */ 56 | protected $modulesNamespace = '\\Webleit\\ZohoBooksApi\\Modules\\Settings\\'; 57 | 58 | /** 59 | * Settings constructor. 60 | * @param Client $client 61 | */ 62 | function __construct(Client $client) 63 | { 64 | $this->client = $client; 65 | } 66 | 67 | /** 68 | * Proxy any module call to the right api call 69 | * @param $name 70 | * @return Module 71 | */ 72 | public function __get($name) 73 | { 74 | return $this->createModule($name); 75 | } 76 | 77 | public function getClient(): Client 78 | { 79 | return $this->client; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/Modules/Settings/AutoReminders.php: -------------------------------------------------------------------------------- 1 | doAction($id, 'enable'); 36 | } 37 | 38 | /** 39 | * @param $id 40 | * @return bool 41 | */ 42 | public function disable($id) 43 | { 44 | return $this->doAction($id, 'disable'); 45 | } 46 | } -------------------------------------------------------------------------------- /src/Modules/Settings/CreditNotes.php: -------------------------------------------------------------------------------- 1 | notesandterms = new NotesAndTerms($client); 25 | } 26 | 27 | /** 28 | * @return string 29 | */ 30 | protected function getAnswerKeyName() 31 | { 32 | return 'creditnote_settings'; 33 | } 34 | } -------------------------------------------------------------------------------- /src/Modules/Settings/CreditNotes/NotesAndTerms.php: -------------------------------------------------------------------------------- 1 | client); 21 | $rates->setParent($this->get($id)); 22 | 23 | return $rates; 24 | } 25 | 26 | /** 27 | * @return string 28 | */ 29 | public function getUrlPath() 30 | { 31 | return 'settings/currencies'; 32 | } 33 | 34 | /** 35 | * @return string 36 | */ 37 | public function getModelClassName() 38 | { 39 | return '\\Webleit\\ZohoBooksApi\\Models\\Settings\\Currency'; 40 | } 41 | } -------------------------------------------------------------------------------- /src/Modules/Settings/Currencies/ExchangeRates.php: -------------------------------------------------------------------------------- 1 | notesandterms = new NotesAndTerms($client); 25 | } 26 | 27 | /** 28 | * @return string 29 | */ 30 | protected function getAnswerKeyName() 31 | { 32 | return 'estimate_settings'; 33 | } 34 | } -------------------------------------------------------------------------------- /src/Modules/Settings/Estimates/NotesAndTerms.php: -------------------------------------------------------------------------------- 1 | notesandterms = new NotesAndTerms($client); 25 | } 26 | 27 | /** 28 | * @return string 29 | */ 30 | protected function getAnswerKeyName() 31 | { 32 | return 'invoice_settings'; 33 | } 34 | } -------------------------------------------------------------------------------- /src/Modules/Settings/Invoices/NotesAndTerms.php: -------------------------------------------------------------------------------- 1 | get(null)]); 45 | } 46 | 47 | /** 48 | * @param string $id 49 | * @param array $params 50 | * @return \Webleit\ZohoBooksApi\Models\Model 51 | */ 52 | public function get ($id, array $params = []) 53 | { 54 | return parent::get($id, $params); // TODO: Change the autogenerated stub 55 | } 56 | 57 | /** 58 | * Update a record for this module 59 | * @param string $id can pass null, only used for compatibility 60 | * @param array $data 61 | * @param array $params 62 | * @return Model 63 | */ 64 | public function update($id, $data, $params = []) 65 | { 66 | // unlike most(all?) other update endpoints, the url is not added to the path 67 | // this is likely because there is only one openingbalances record w multiple accounts 68 | // https://www.zoho.com/books/api/v3/opening-balance/#update-opening-balance 69 | $data = $this->client->call($this->getUrl(), 'PUT', $data, $params); 70 | $data = $data[$this->inflector->singularize($this->getResourceItemKey())]; 71 | 72 | return $this->make($data); 73 | } 74 | 75 | /** 76 | * Deletes a record for this module 77 | * 78 | * @param $id can pass null, only used for compatibility 79 | * @return bool 80 | */ 81 | public function delete($id) 82 | { 83 | // unlike most(all?) other update endpoints, the url is not added to the path 84 | // this is likely because there is only one openingbalances record w multiple accounts 85 | // https://www.zoho.com/books/api/v3/opening-balance/#delete-opening-balance 86 | $data = $this->client->call($this->getUrl(), 'DELETE'); 87 | 88 | // all is ok if we've reached this point 89 | return true; 90 | } 91 | 92 | } 93 | -------------------------------------------------------------------------------- /src/Modules/Settings/Preferences.php: -------------------------------------------------------------------------------- 1 | getShortName(); 20 | return parent::getModelClassName() . '\\' . $this->inflector->singularize($this->inflector->camelize($className)); 21 | } 22 | 23 | /** 24 | * @return string 25 | */ 26 | public function getUrlPath() 27 | { 28 | $path = $this->getParent()->getModule()->getUrlPath() . '/' . $this->getParent()->getId() . '/'; 29 | $path .= parent::getUrlPath(); 30 | 31 | return $path; 32 | } 33 | 34 | /** 35 | * @var Model 36 | */ 37 | protected $parent; 38 | 39 | /** 40 | * @param Model $parent 41 | */ 42 | public function setParent(Model $parent) 43 | { 44 | $this->parent = $parent; 45 | } 46 | 47 | /** 48 | * @return Model 49 | */ 50 | public function getParent() 51 | { 52 | return $this->parent; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Modules/Users.php: -------------------------------------------------------------------------------- 1 | client->get($this->getUrl() . '/me'); 18 | return new User($data['user'], $this); 19 | } 20 | 21 | /** 22 | * @param $id 23 | * @return bool 24 | */ 25 | public function invite($id) 26 | { 27 | return $this->doAction($id, 'invite'); 28 | } 29 | 30 | /** 31 | * @param $id 32 | * @return bool 33 | */ 34 | public function markAsActive($id) 35 | { 36 | return $this->doAction($id, 'active'); 37 | } 38 | 39 | /** 40 | * @param $id 41 | * @return bool 42 | */ 43 | public function markAsInactive($id) 44 | { 45 | return $this->doAction($id, 'inactive'); 46 | } 47 | } -------------------------------------------------------------------------------- /src/Modules/VendorCredits.php: -------------------------------------------------------------------------------- 1 | client->post($this->getUrl() . '/' . $id . '/bills', $data); 26 | 27 | return $data['bills']; 28 | } 29 | 30 | /** 31 | * @param $id 32 | * @return Collection 33 | */ 34 | public function getBills($id) 35 | { 36 | $class = '\\Webleit\\ZohoBooksApi\\Models\\VendorCredit\\Bill'; 37 | return $this->getPropertyList('applytobills', $id, $class, 'bills_credited'); 38 | } 39 | 40 | /** 41 | * @param $id 42 | * @param $idOrBill 43 | * @return bool 44 | */ 45 | public function deleteFromBill($id, $idOrBill) 46 | { 47 | if ($idOrBill instanceof VendorCredit\Bill) { 48 | $idOrBill = $idOrBill->getId(); 49 | } 50 | 51 | $this->client->delete($this->getUrl() . '/' . $id . '/bills/' . $idOrBill); 52 | // If we arrive here without exceptions, everything went well 53 | return true; 54 | } 55 | 56 | /** 57 | * @param $id string 58 | * @return boolean 59 | */ 60 | public function markAsOpen($id) 61 | { 62 | return $this->markAs($id, 'open'); 63 | } 64 | 65 | /** 66 | * @param $id string 67 | * @return boolean 68 | */ 69 | public function markAsVoid($id) 70 | { 71 | return $this->markAs($id, 'void'); 72 | } 73 | 74 | /** 75 | * Override returned key 76 | * 77 | * This overrides the key that is returned from zoho, as they 78 | * send back 'vendor_credits' instead of 'vendorcredits' 79 | * 80 | * @return string 81 | */ 82 | public function getResourceKey() 83 | { 84 | return 'vendor_credits'; 85 | } 86 | 87 | /** 88 | * Override Api Key Name 89 | * 90 | * This overrides the key that is returned from zoho, as they 91 | * send back 'vendor_credit' instead of 'vendorcredit' 92 | * 93 | * @return string 94 | */ 95 | 96 | public function getApiKeyName() 97 | { 98 | return 'vendor_credit_id'; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/Modules/VendorPayments.php: -------------------------------------------------------------------------------- 1 | refunds = new Refunds($client); 26 | } 27 | 28 | /** 29 | * @return string 30 | */ 31 | public function getName() 32 | { 33 | return 'payment'; 34 | } 35 | 36 | /** 37 | * @return string 38 | */ 39 | public function getResourceKey() 40 | { 41 | return 'vendorpayments'; 42 | } 43 | 44 | /** 45 | * @return string 46 | */ 47 | public function getResourceItemKey() 48 | { 49 | return 'vendorpayment'; 50 | } 51 | 52 | /** 53 | * @param $id 54 | * @return \Illuminate\Support\Collection 55 | */ 56 | public function getRefunds($id) 57 | { 58 | $className = $this->getModelClassName() . '\\Refund'; 59 | return $this->getPropertyList('refunds', $id, $className, 'vendorpayment_refunds', $this->refunds); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Modules/VendorPayments/Refunds.php: -------------------------------------------------------------------------------- 1 | perPage = $params['per_page'] ?? $this->perPage; 17 | $this->page = $params['page'] ?? $this->page; 18 | $this->moreRecords = $params['has_more_page'] ?? $this->moreRecords; 19 | $this->total = $params['total'] ?? null; 20 | $this->totalPages = $params['total_pages'] ?? null; 21 | } 22 | 23 | public function perPage(): int 24 | { 25 | return $this->perPage; 26 | } 27 | 28 | public function page(): int 29 | { 30 | return $this->page; 31 | } 32 | 33 | public function hasMoreRecords(): bool 34 | { 35 | return $this->moreRecords; 36 | } 37 | 38 | /** 39 | * Get the value of total 40 | * 41 | * @return int|null 42 | */ 43 | public function total(): ?int 44 | { 45 | return $this->total; 46 | } 47 | 48 | /** 49 | * Get the value of totalPages 50 | * 51 | * @return int|null 52 | */ 53 | public function totalPages(): ?int 54 | { 55 | return $this->totalPages; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/ZohoBooks.php: -------------------------------------------------------------------------------- 1 | Modules\Contacts::class, 54 | 'estimates' => Modules\Estimates::class, 55 | 'salesorders' => Modules\SalesOrders::class, 56 | 'invoices' => Modules\Invoices::class, 57 | 'recurringinvoices' => Modules\RecurringInvoices::class, 58 | 'creditnotes' => Modules\CreditNotes::class, 59 | 'customerpayments' => Modules\CustomerPayments::class, 60 | 'expenses' => Modules\Expenses::class, 61 | 'recurringexpenses' => Modules\RecurringExpenses::class, 62 | 'purchaseorders' => Modules\PurchaseOrders::class, 63 | 'bills' => Modules\Bills::class, 64 | 'vendorcredits' => Modules\VendorCredits::class, 65 | 'vendorpayments' => Modules\VendorPayments::class, 66 | 'bankaccounts' => Modules\BankAccounts::class, 67 | 'banktransactions' => Modules\BankTransactions::class, 68 | 'bankrules' => Modules\BankRules::class, 69 | 'chartofaccounts' => Modules\ChartOfAccounts::class, 70 | 'journals' => Modules\Journals::class, 71 | 'basecurrencyadjustment' => Modules\BaseCurrencyAdjustment::class, 72 | 'projects' => Modules\Projects::class, 73 | 'settings' => Modules\Settings::class, 74 | 'organizations' => Modules\Organizations::class, 75 | 'items' => Modules\Items::class, 76 | 'users' => Modules\Users::class, 77 | 'import' => Modules\Import::class, 78 | ]; 79 | 80 | public function __construct(Client $client) 81 | { 82 | $this->client = $client; 83 | } 84 | 85 | public function __get(string $name): Contracts\Module 86 | { 87 | return $this->createModule($name); 88 | } 89 | 90 | public function getClient(): Client 91 | { 92 | return $this->client; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /tests/ApiTest.php: -------------------------------------------------------------------------------- 1 | organizations->getList(); 22 | $this->assertTrue(count($list) > 0); 23 | } 24 | 25 | /** 26 | * @test 27 | */ 28 | public function canGetListOfContacts() 29 | { 30 | $list = self::$zoho->contacts->getList(); 31 | $this->assertTrue(count($list) > 0); 32 | } 33 | 34 | /** 35 | * @test 36 | */ 37 | public function canGetListOfCustomerPayments() 38 | { 39 | /** @var Collection $list */ 40 | $list = self::$zoho->customerpayments->getList(); 41 | $this->assertTrue(count($list) > 0); 42 | 43 | /** @var CustomerPayment $item */ 44 | $item = $list->first(); 45 | 46 | $itemRetrieved = self::$zoho->customerpayments->get($item->getId()); 47 | 48 | $this->assertEquals(CustomerPayment::class, get_class($itemRetrieved)); 49 | $this->assertEquals($item->getId(), $itemRetrieved->getId()); 50 | } 51 | 52 | /** 53 | * @test 54 | */ 55 | public function canCreateAndDeleteCustomer() 56 | { 57 | $name = 'Test ' . uniqid(); 58 | /** @var Contact $customer */ 59 | $customer = self::$zoho->contacts->create([ 60 | 'contact_name' => $name 61 | ]); 62 | 63 | $this->assertEquals(Contact::class, get_class($customer)); 64 | $this->assertEquals($name, $customer->toArray()['contact_name']); 65 | 66 | $this->assertTrue(self::$zoho->contacts->delete($customer->getId())); 67 | } 68 | 69 | /** 70 | * @test 71 | */ 72 | public function canCreateAndFilterCustomer() 73 | { 74 | $name = 'Test ' . uniqid(); 75 | /** @var Contact $customer */ 76 | $customer = self::$zoho->contacts->create([ 77 | 'contact_name' => $name 78 | ]); 79 | 80 | $customers = self::$zoho->contacts->getList([ 81 | 'contact_name' => $name 82 | ]); 83 | 84 | $this->assertEquals(1, $customers->count()); 85 | 86 | // delete it it afterwards 87 | self::$zoho->contacts->delete($customer->getId()); 88 | } 89 | 90 | /** 91 | * @test 92 | */ 93 | public function canExportListOfInvoices() 94 | { 95 | $id =self::$zoho->invoices->getList()->first()->getId(); 96 | $list = self::$zoho->invoices->exportPdfList([$id]); 97 | $this->assertNotNull($list); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /tests/ClassNameGeneratorTest.php: -------------------------------------------------------------------------------- 1 | assert_that_provided_modules_exists(self::$zoho); 23 | } 24 | 25 | /** 26 | * @test 27 | */ 28 | public function customer_payments_exists() 29 | { 30 | // Create a stub for the class. 31 | $zohoBooks = self::$zoho; 32 | $customerPayments = $zohoBooks->customerpayments; 33 | $customerPayment = $customerPayments->make([]); 34 | 35 | $this->assertEquals( get_class($customerPayment),CustomerPayment::class); 36 | } 37 | 38 | /** 39 | * @param ProvidesModules $class 40 | */ 41 | protected function assert_that_provided_modules_exists(ProvidesModules $class) 42 | { 43 | foreach ($class->getAvailableModuleKeys() as $module) { 44 | 45 | /** @var Module $moduleClass */ 46 | $moduleClass = $class->createModule($module); 47 | 48 | $this->assertTrue($moduleClass instanceof \Webleit\ZohoBooksApi\Contracts\Module, $module); 49 | 50 | if ($moduleClass instanceof Module && $moduleClass !== Import::class) { 51 | $this->assertTrue(class_exists($moduleClass->getModelClassName()), $moduleClass->getModelClassName()); 52 | } 53 | 54 | if ($moduleClass instanceof ProvidesModules) { 55 | $this->assert_that_provided_modules_exists($moduleClass); 56 | } 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | setOrganizationId($auth->organization_id); 41 | 42 | $client = new ZohoBooks($client); 43 | 44 | self::$zoho = $client; 45 | self::$client = $client->getClient(); 46 | } 47 | 48 | protected static function createOAuthClient(): OAuthClient 49 | { 50 | $authFile = __DIR__ . '/config.example.json'; 51 | if (file_exists(__DIR__ . '/config.json')) { 52 | $authFile = __DIR__ . '/config.json'; 53 | } 54 | 55 | $auth = json_decode(file_get_contents($authFile)); 56 | 57 | $region = Region::US; 58 | if (isset($auth->region)) { 59 | $region = Region::make($auth->region); 60 | } 61 | 62 | $filesystemAdapter = new Local(sys_get_temp_dir()); 63 | $filesystem = new Filesystem($filesystemAdapter); 64 | $pool = new FilesystemCachePool($filesystem); 65 | 66 | $client = new OAuthClient($auth->client_id, $auth->client_secret); 67 | $client->setRefreshToken($auth->refresh_token); 68 | $client->setRegion($region); 69 | $client->offlineMode(); 70 | $client->useCache($pool); 71 | 72 | return $client; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /tests/config.example.json: -------------------------------------------------------------------------------- 1 | { 2 | "client_id" : "", 3 | "client_secret": "", 4 | "grant_token": "", 5 | "refresh_token" : "", 6 | "access_token": "", 7 | "auth_token": "", 8 | "organization_id": "" 9 | } --------------------------------------------------------------------------------