BxXeroApi.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415
  1. <?php defined('BX_DOL') or die('hack attempt');
  2. /**
  3. * Copyright (c) UNA, Inc - https://una.io
  4. * MIT License - https://opensource.org/licenses/MIT
  5. *
  6. * @defdroup Xero Xero
  7. * @indroup UnaModules
  8. *
  9. * @{
  10. *
  11. * @see https://github.com/XeroAPI/xero-php-oauth2
  12. * @see https://xeroapi.github.io/xero-php-oauth2/docs/v2/accounting/index.html
  13. */
  14. require __DIR__ . '/../plugins/autoload.php';
  15. use XeroAPI\XeroPHP\AccountingObjectSerializer;
  16. class BxXeroApi extends BxDol
  17. {
  18. protected $_oModule;
  19. protected $_oProvider;
  20. public function __construct(&$oModule)
  21. {
  22. $this->_oModule = $oModule;
  23. }
  24. public function authorize()
  25. {
  26. $oProvider = $this->_getProvider();
  27. $aOptions = [
  28. 'scope' => ['openid email profile offline_access assets projects accounting.settings accounting.transactions accounting.contacts accounting.journals.read accounting.reports.read accounting.attachments']
  29. ];
  30. $sAuthorizationUrl = $oProvider->getAuthorizationUrl($aOptions);
  31. $this->_oModule->_oConfig->setState($oProvider->getState());
  32. return $sAuthorizationUrl;
  33. }
  34. public function isAuthorized()
  35. {
  36. $sTenantId = $this->_oModule->_oConfig->getTenantId();
  37. if($sTenantId === false)
  38. return false;
  39. if(!$this->_oModule->_oConfig->isDataExpired())
  40. return true;
  41. $sRefreshToken = $this->_oModule->_oConfig->getRefreshToken();
  42. if($sRefreshToken === false)
  43. return false;
  44. $oAccessToken = $this->_getProvider()->getAccessToken('refresh_token', [
  45. 'refresh_token' => $sRefreshToken
  46. ]);
  47. $this->_oModule->_oConfig->setData(
  48. $oAccessToken->getToken(),
  49. $oAccessToken->getExpires(),
  50. $sTenantId,
  51. $oAccessToken->getRefreshToken(),
  52. $oAccessToken->getValues()['id_token']
  53. );
  54. return true;
  55. }
  56. public function callback($sCode)
  57. {
  58. try {
  59. $oProvider = $this->_getProvider();
  60. $oAccessToken = $oProvider->getAccessToken('authorization_code', [
  61. 'code' => $sCode
  62. ]);
  63. $sAccessToken = (string)$oAccessToken->getToken();
  64. $oXeroConfig = XeroAPI\XeroPHP\Configuration::getDefaultConfiguration()->setAccessToken($sAccessToken);
  65. $oIdentity = new XeroAPI\XeroPHP\Api\IdentityApi(new GuzzleHttp\Client(), $oXeroConfig);
  66. $aResults = $oIdentity->getConnections();
  67. $this->_oModule->_oConfig->setData(
  68. $oAccessToken->getToken(),
  69. $oAccessToken->getExpires(),
  70. $aResults[0]->getTenantId(),
  71. $oAccessToken->getRefreshToken(),
  72. $oAccessToken->getValues()['id_token']
  73. );
  74. return $this->_oModule->_oConfig->getAuthorizeUrl(0);
  75. }
  76. catch (\League\OAuth2\Client\Provider\Exception\IdentityProviderException $oException) {
  77. bx_log($this->_oModule->getName(), ":\n[API] Callback failed: " . $oException->getMessage(), BX_LOG_ERR);
  78. return false;
  79. }
  80. }
  81. public function actionTest()
  82. {
  83. $mixedApi = $this->_getApiObject();
  84. if($mixedApi === false)
  85. return false;
  86. list($sTenantId, $oXeroApi) = $mixedApi;
  87. $oXeroResponse = $oXeroApi->getOrganisations($sTenantId);
  88. return $oXeroResponse->getOrganisations()[0]->getName();
  89. }
  90. public function actionAddContact($iProfileId, $sProfileName, $sProfileEmail)
  91. {
  92. $mixedApi = $this->_getApiObject();
  93. if($mixedApi === false)
  94. return false;
  95. list($sTenantId, $oXeroApi) = $mixedApi;
  96. $mixedResult = false;
  97. try {
  98. $oContact = new XeroAPI\XeroPHP\Models\Accounting\Contact;
  99. $oContact->setName($sProfileName)
  100. ->setEmailAddress($sProfileEmail)
  101. ->setContactNumber($iProfileId);
  102. $aContacts = [$oContact];
  103. $oContacts = new XeroAPI\XeroPHP\Models\Accounting\Contacts;
  104. $oContacts->setContacts($aContacts);
  105. $oXeroResponse = $oXeroApi->createContacts($sTenantId, $oContacts);
  106. if(count($oXeroResponse->getContacts()) == 1) {
  107. $mixedResult = $oXeroResponse->getContacts()[0]->getContactId();
  108. $this->_oModule->_oDb->insertContact([
  109. 'email' => $sProfileEmail,
  110. 'contact' => $mixedResult,
  111. 'added' => time()
  112. ]);
  113. }
  114. }
  115. catch (Exception $oException) {
  116. bx_log($this->_oModule->getName(), ":\n[API] Add Contact: " . $oException->getMessage(), BX_LOG_ERR);
  117. }
  118. return $mixedResult;
  119. }
  120. public function actionGetContact($sProfileEmail)
  121. {
  122. $mixedApi = $this->_getApiObject();
  123. if($mixedApi === false)
  124. return false;
  125. list($sTenantId, $oXeroApi) = $mixedApi;
  126. $mixedResult = false;
  127. try {
  128. $oXeroResponse = $oXeroApi->getContacts($sTenantId, null, 'EmailAddress="' . $sProfileEmail . '"');
  129. if(count($oXeroResponse->getContacts()) == 1 && $oXeroResponse->getContacts()[0]->getEmailAddress() == $sProfileEmail)
  130. $mixedResult = $oXeroResponse->getContacts()[0];
  131. }
  132. catch (Exception $oException) {
  133. bx_log($this->_oModule->getName(), ":\n[API] Get Contact: " . $oException->getMessage(), BX_LOG_ERR);
  134. }
  135. return $mixedResult;
  136. }
  137. /**
  138. * Get contacts by parameter(s).
  139. *
  140. * @param DateTime $oDate - Only records created or modified since this timestamp will be returned. Example: new DateTime("2019-01-02T19:20:30+01:00")
  141. * @param string $sWhere
  142. * @param string $sOrder
  143. * @param string $aIds - Filter by a comma-separated list of Invoice Ids.
  144. * @param int $iPage - e.g. page=1 – Up to 100 invoices will be returned in a single API call with line items.
  145. * @param bool $bIncludeArchived - e.g. includeArchived=true - Contacts with a status of ARCHIVED will be included
  146. * @return array of contacts or boolean false on failure.
  147. */
  148. public function actionGetContacts($oDate = null, $sWhere = null, $sOrder = null, $aIds = null, $iPage = 1, $bIncludeArchived = false)
  149. {
  150. $mixedApi = $this->_getApiObject();
  151. if($mixedApi === false)
  152. return false;
  153. list($sTenantId, $oXeroApi) = $mixedApi;
  154. $mixedResult = false;
  155. try {
  156. $oXeroResponse = $oXeroApi->getContacts($sTenantId, $oDate, $sWhere, $sOrder, $aIds, $iPage, $bIncludeArchived);
  157. if(count($oXeroResponse->getContacts()) > 0)
  158. $mixedResult = $oXeroResponse->getContacts();
  159. }
  160. catch (Exception $oException) {
  161. bx_log($this->_oModule->getName(), ":\n[API] Get Contacts: " . $oException->getMessage(), BX_LOG_ERR);
  162. }
  163. return $mixedResult;
  164. }
  165. public function actionIsContact($sProfileEmail)
  166. {
  167. $mixedResult = $this->actionGetContact($sProfileEmail);
  168. if($mixedResult === false)
  169. return $mixedResult;
  170. if(!$this->_oModule->_oDb->isContact($sProfileEmail))
  171. $this->_oModule->_oDb->insertContact([
  172. 'email' => $sProfileEmail,
  173. 'contact' => $mixedResult->getContactId(),
  174. 'added' => time()
  175. ]);
  176. return true;
  177. }
  178. /**
  179. *
  180. * @param type $sProfileEmail
  181. * @param type $sName
  182. * @param type $mixedAmount - float Amount or an array with float Amount and float Tax Amount.
  183. * @param type $iQuantity
  184. * @param mixed $mixedDueDate - e.g. string '2021-12-01' or a date object
  185. * @param type $sAccount
  186. * @return invoice ID or boolean false on failure.
  187. */
  188. public function actionAddInvoice($sProfileEmail, $sName, $mixedAmount, $iQuantity = 1, $mixedDueDate = false, $sAccount = BX_XERO_ACCOUNT_CODE_SALES)
  189. {
  190. $mixedApi = $this->_getApiObject();
  191. if($mixedApi === false)
  192. return false;
  193. list($sTenantId, $oXeroApi) = $mixedApi;
  194. $mixedResult = false;
  195. try {
  196. if(empty($mixedDueDate))
  197. $mixedDueDate = date_add(new DateTime(), new DateInterval('P1M'));
  198. else if(is_string($mixedDueDate))
  199. $mixedDueDate = new DateTime($mixedDueDate);
  200. $fAmount = $mixedAmount;
  201. $fTaxAmount = $sTaxType = false;
  202. if(is_array($mixedAmount))
  203. switch(count($mixedAmount)) {
  204. case 2:
  205. list($fAmount, $fTaxAmount) = $mixedAmount;
  206. break;
  207. case 3:
  208. list($fAmount, $fTaxAmount, $sTaxType) = $mixedAmount;
  209. break;
  210. }
  211. $sLineAmountType = \XeroAPI\XeroPHP\Models\Accounting\LineAmountTypes::NO_TAX;
  212. $oLineItem = new XeroAPI\XeroPHP\Models\Accounting\LineItem;
  213. $oLineItem->setDescription($sName)
  214. ->setUnitAmount($fAmount)
  215. ->setQuantity($iQuantity)
  216. ->setAccountCode($sAccount);
  217. if($fTaxAmount !== false) {
  218. $sLineAmountType = \XeroAPI\XeroPHP\Models\Accounting\LineAmountTypes::EXCLUSIVE;
  219. $oLineItem->setTaxAmount($fTaxAmount);
  220. }
  221. if($sTaxType !== false)
  222. $oLineItem->setTaxType($sTaxType);
  223. $aLineItems = [$oLineItem];
  224. $oContact = $this->actionGetContact($sProfileEmail);
  225. if(!$oContact)
  226. return false;
  227. $oInvoiceContact = new XeroAPI\XeroPHP\Models\Accounting\Contact;
  228. $oInvoiceContact->setContactId($oContact->getContactId());
  229. $oInvoice = new XeroAPI\XeroPHP\Models\Accounting\Invoice;
  230. $oInvoice->setReference('Ref-' . $this->_getRandNum())
  231. ->setDueDate($mixedDueDate)
  232. ->setContact($oInvoiceContact)
  233. ->setLineItems($aLineItems)
  234. ->setStatus(XeroAPI\XeroPHP\Models\Accounting\Invoice::STATUS_AUTHORISED)
  235. ->setType(XeroAPI\XeroPHP\Models\Accounting\Invoice::TYPE_ACCREC)
  236. ->setLineAmountTypes($sLineAmountType);
  237. $oXeroResponse = $oXeroApi->createInvoices($sTenantId, $oInvoice);
  238. if(count($oXeroResponse->getInvoices()) == 1)
  239. $mixedResult = $oXeroResponse->getInvoices()[0]->getInvoiceId();
  240. }
  241. catch (Exception $oException) {
  242. bx_log($this->_oModule->getName(), ":\n[API] Add Invoice: " . $oException->getMessage(), BX_LOG_ERR);
  243. }
  244. return $mixedResult;
  245. }
  246. /**
  247. * Note. Isn't used for now.
  248. */
  249. public function actionGetInvoices($oDate = null, $sWhere = null, $sOrder = null, $aIds = null, $aNumbers = null, $aContactIds = null, $aStatuses = null, $iPage = 1, $bIncludeArchived = false)
  250. {
  251. $mixedApi = $this->_getApiObject();
  252. if($mixedApi === false)
  253. return false;
  254. list($sTenantId, $oXeroApi) = $mixedApi;
  255. $mixedResult = false;
  256. try {
  257. $oXeroResponse = $oXeroApi->getInvoices($sTenantId, $oDate, $sWhere, $sOrder, $aIds, $aNumbers, $aContactIds, $aStatuses, $iPage, $bIncludeArchived);
  258. if(count($oXeroResponse->getInvoices()) > 0)
  259. $mixedResult = $oXeroResponse->getInvoices();
  260. }
  261. catch (Exception $oException) {
  262. bx_log($this->_oModule->getName(), ":\n[API] Get Invoices: " . $oException->getMessage(), BX_LOG_ERR);
  263. }
  264. return $mixedResult;
  265. }
  266. public function actionGetInvoice($sId, $iUnitDp = 2)
  267. {
  268. $mixedApi = $this->_getApiObject();
  269. if($mixedApi === false)
  270. return false;
  271. list($sTenantId, $oXeroApi) = $mixedApi;
  272. $mixedResult = false;
  273. try {
  274. $oXeroResponse = $oXeroApi->getInvoice($sTenantId, $sId, $iUnitDp);
  275. if(count($oXeroResponse->getInvoices()) == 1)
  276. $mixedResult = $oXeroResponse->getInvoices()[0];
  277. }
  278. catch (Exception $oException) {
  279. bx_log($this->_oModule->getName(), ":\n[API] Get Invoice: " . $oException->getMessage(), BX_LOG_ERR);
  280. }
  281. return $mixedResult;
  282. }
  283. public function sendInvoice($sInvoiceId)
  284. {
  285. $mixedApi = $this->_getApiObject();
  286. if($mixedApi === false)
  287. return false;
  288. list($sTenantId, $oXeroApi) = $mixedApi;
  289. $bResult = true;
  290. try {
  291. $oRequestEmpty = new XeroAPI\XeroPHP\Models\Accounting\RequestEmpty;
  292. $oXeroApi->emailInvoice($sTenantId, $sInvoiceId, $oRequestEmpty);
  293. }
  294. catch (Exception $oException) {
  295. bx_log($this->_oModule->getName(), ":\n[API] Send Invoice: " . $oException->getMessage(), BX_LOG_ERR);
  296. $bResult = false;
  297. }
  298. return $bResult;
  299. }
  300. protected function _getProvider()
  301. {
  302. if(empty($this->_oProvider))
  303. $this->_oProvider = new \League\OAuth2\Client\Provider\GenericProvider([
  304. 'clientId' => $this->_oModule->_oConfig->getClientId(),
  305. 'clientSecret' => $this->_oModule->_oConfig->getClientSecret(),
  306. 'redirectUri' => $this->_oModule->_oConfig->getRedirectUrl(),
  307. 'urlAuthorize' => 'https://login.xero.com/identity/connect/authorize',
  308. 'urlAccessToken' => 'https://identity.xero.com/connect/token',
  309. 'urlResourceOwnerDetails' => 'https://api.xero.com/api.xro/2.0/Organisation'
  310. ]);
  311. return $this->_oProvider;
  312. }
  313. protected function _getApiObject()
  314. {
  315. $sTenantId = $this->_oModule->_oConfig->getTenantId();
  316. if($sTenantId === false) {
  317. bx_log($this->_oModule->getName(), ":\n[API] Fetch failed: Empty Tenant ID.", BX_LOG_ERR);
  318. return false;
  319. }
  320. if(!$this->isAuthorized()) {
  321. bx_log($this->_oModule->getName(), ":\n[API] Fetch failed: Unauthorized access.", BX_LOG_ERR);
  322. return false;
  323. }
  324. $sAccessToken = $this->_oModule->_oConfig->getAccessToken();
  325. $oXeroConfig = XeroAPI\XeroPHP\Configuration::getDefaultConfiguration()->setAccessToken($sAccessToken);
  326. $oXeroApi = new XeroAPI\XeroPHP\Api\AccountingApi(
  327. new GuzzleHttp\Client(),
  328. $oXeroConfig
  329. );
  330. return array($sTenantId, $oXeroApi);
  331. }
  332. public function _getRandNum()
  333. {
  334. return strval(rand(1000, 100000));
  335. }
  336. }