src/Service/XeroService.php line 140

Open in your IDE?
  1. <?php
  2. namespace App\Service;
  3. use App\Entity\User;
  4. use App\Entity\Currency;
  5. use App\Entity\Department;
  6. use App\Entity\ExpenseRequest;
  7. use App\Entity\ExpenseRequestInvoice;
  8. use App\Entity\SalesOrder;
  9. use App\Entity\XeroAuth;
  10. use App\Entity\Invoice;
  11. use App\Entity\VendorInvoice;
  12. use App\Entity\XeroCreditNoteAllocation;
  13. use App\Entity\XeroLineItem;
  14. use App\Entity\XeroContact;
  15. use App\Entity\XeroCreditNote;
  16. use App\Entity\XeroWebhook;
  17. use App\Entity\XeroUser;
  18. use App\Entity\XeroOrganisation;
  19. use App\Entity\Project;
  20. use App\Service\CurrencyService;
  21. use App\Service\MailgunService;
  22. use App\Service\ProjectService;
  23. use Doctrine\ORM\EntityManagerInterface;
  24. use GuzzleHttp\Exception\GuzzleException;
  25. use Psr\Log\LoggerInterface;
  26. use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
  27. use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
  28. use Symfony\Component\Routing\RouterInterface;
  29. use Symfony\Component\HttpFoundation\JsonResponse;
  30. use Symfony\Contracts\Translation\TranslatorInterface;
  31. use Exception;
  32. use XeroAPI\XeroPHP\Configuration;
  33. use XeroAPI\XeroPHP\AccountingObjectSerializer;
  34. use XeroAPI\XeroPHP\Models\Accounting\Contact;
  35. use XeroAPI\XeroPHP\Models\Accounting\Contacts;
  36. class XeroService
  37. {
  38.     private $em;
  39.     private $params;
  40.     private $logger;
  41.     private $currencyService;
  42.     private $mailgunService;
  43.     private $projectService;
  44.     private $urlGenerator;
  45.     private $router;
  46.     private $arr_item_codes;
  47.     private $arr_excluded_item_codes;
  48.     private $mattermostService;
  49.     private $translator;
  50.     private $projectIdPattern;
  51.     public function __construct(
  52.         EntityManagerInterface $entityManager,
  53.         ParameterBagInterface $params,
  54.         LoggerInterface $logger,
  55.         CurrencyService $currencyService,
  56.         MailgunService $mailgunService,
  57.         ProjectService $projectService,
  58.         UrlGeneratorInterface $urlGenerator,
  59.         RouterInterface $router,
  60.         MattermostService $mattermostService,
  61.         TranslatorInterface $translator
  62.     ) {
  63.         $this->em $entityManager;
  64.         $this->params $params;
  65.         $this->logger $logger;
  66.         $this->currencyService $currencyService;
  67.         $this->mailgunService $mailgunService;
  68.         $this->projectService $projectService;
  69.         $this->urlGenerator $urlGenerator;
  70.         $this->router $router;
  71.         $this->translator $translator;
  72.         $this->arr_item_codes = [
  73.             "account agency fees" => 14// 14 = Account Management Dept.
  74.             "account agency fee" => 14// Renamed to Account Agency fees
  75.             "strategy" => 14,
  76.             "content" => 2// 2 = Creative Dept.
  77.             "content production" => 2,
  78.             "creative production" => 2,
  79.             "edm" => 2,
  80.             "photo" => 2,
  81.             "photography" => 2// to prevent typo   
  82.             "video" => 2// Renamed
  83.             "videography" => 2,
  84.             "d&a services" => 9// 9 = Data Dept.
  85.             "media agency fees" => 3// 3 = Media Dept.
  86.             "media buy" => 3,
  87.             "tracking" => 3,
  88.             "social media services" => 3,
  89.             "3rd party" => null,
  90.             "incentive management-pt" => null,
  91.             "incentive mgmt-agency fee(mc)" => 14,
  92.             "incentive mgmt-platformfee(mc)" => 14,
  93.             "incentive mgmt-platform fee(mc)" => 14,
  94.             "platform fee (mc)" => 14,
  95.             "inter company billing" => null// need to be checked
  96.             "research" => 19// 19 = Research Dept.
  97.             "market research" => 19,
  98.             "social" => null,
  99.             "training" => null,
  100.             "seo" => 8// 8 = SEO Dept.
  101.             "hosting" => 1// 1 = Tech Dept.
  102.             "maintenance" => 1,
  103.             "mobile app" => 1,
  104.             "mobile dev" => 1// Renamed to Mobile App
  105.             "mobile development" => 1// Renamed to Mobile App
  106.             "web development" => 1,
  107.             "discount" => null,
  108.             "uxui audit & analysis" => 20,
  109.             "uxui workshop" => 20,
  110.             "uxui strategy" => 20,
  111.             "uxui design" => 20,
  112.             "uxui training" => 20,
  113.             "uxui research" => 20,
  114.             "uxui fullstack programme" => 20,
  115.             "uxui project management" => 20,
  116.             // "Branding" => 14, // Old Code
  117.             // "Community Management" => 14, // Old Code
  118.             // "Computer" => null, // Old Code
  119.             // "Taxi" => null, // Old Code
  120.             // "Travel" => null, // Old Code
  121.         ];
  122.         $this->arr_excluded_item_codes = ["3rd party""discount""media buy""tracking""incentive management-pt"];
  123.         $this->mattermostService $mattermostService;
  124.         $this->projectIdPattern '/Project\s*Id\s*:?\s*(\d{4}-\d{4})/i';
  125.     }
  126.     public function getSession()
  127.     {
  128.         return $this->em->getRepository(XeroAuth::class)->findAll()[0];
  129.     }
  130.     public function setToken($token$expires null$tenantId$refreshToken$idToken)
  131.     {
  132.         $xeroAuth $this->getSession();
  133.         if (!$xeroAuth) {
  134.             $xeroAuth = new XeroAuth();
  135.         }
  136.         $xeroAuth->setToken($token);
  137.         $xeroAuth->setTenantId($tenantId);
  138.         $xeroAuth->setExpires($expires);
  139.         $xeroAuth->setRefreshToken($refreshToken);
  140.         $xeroAuth->setIdToken($idToken);
  141.         $this->em->persist($xeroAuth);
  142.         $this->em->flush();
  143.         $this->em->refresh($xeroAuth);
  144.     }
  145.     public function getToken()
  146.     {
  147.         if (is_null($this->getSession())) {
  148.             return null;
  149.         }
  150.         if ($this->getSession()->getExpires() <= time()) {
  151.             return null;
  152.         }
  153.         return $this->getSession();
  154.     }
  155.     public function getAccessToken()
  156.     {
  157.         return $this->getSession()->getAccessToken();
  158.     }
  159.     public function getRefreshToken()
  160.     {
  161.         return $this->getSession()->getRefreshToken();
  162.     }
  163.     public function getExpires()
  164.     {
  165.         return $this->getSession()->getExpires();
  166.     }
  167.     public function getTenantId($countryId)
  168.     {
  169.         $tenantId null;
  170.         switch ($countryId) {
  171.             case 'ID':
  172.                 $tenantId $this->params->get('xeroTenantIdBali');
  173.                 break;
  174.             case 'HK':
  175.                 $tenantId $this->params->get('xeroTenantIdHK');
  176.                 break;
  177.             default:
  178.                 $tenantId $this->params->get('xeroTenantId');
  179.                 break;
  180.         }
  181.         return $tenantId;
  182.     }
  183.     public function fetchOrganisation($id)
  184.     {
  185.         if ($this->getHasExpired()) {
  186.             $this->renewToken($this->params);
  187.         }
  188.         $tenantId = (strlen($id) <= 2) ? $this->getTenantId($id) : $id;
  189.         $config \XeroAPI\XeroPHP\Configuration::getDefaultConfiguration()->setAccessToken((string) $this->getSession()->getToken());
  190.         $accountingApi = new \XeroAPI\XeroPHP\Api\AccountingApi(
  191.             new \GuzzleHttp\Client(),
  192.             $config
  193.         );
  194.         try {
  195.             $xeroCurrencies $accountingApi->getCurrencies($tenantId);
  196.             $xeroCurrencies $xeroCurrencies->getCurrencies();
  197.             $organizations $accountingApi->getOrganisations($tenantId);
  198.             $organization $organizations->getOrganisations()[0];
  199.             $xeroOrganisation $this->em->getRepository(XeroOrganisation::class)->findOneBy(['tenantId' => $tenantId]);
  200.             if (!$xeroOrganisation) {
  201.                 $xeroOrganisation = new XeroOrganisation();
  202.                 $xeroOrganisation->setTenantId($tenantId);
  203.                 $xeroOrganisation->setName($organization->getName());
  204.                 $xeroOrganisation->setCountryCode($organization->getCountryCode());
  205.             }
  206.             $xeroOrganisation->setBaseCurrency($organization->getBaseCurrency());
  207.             $baseCurrency $organization->getBaseCurrency();
  208.             $currencies array_map(function ($currency) use ($baseCurrency) {
  209.                 return [
  210.                     'code' => $currency->getCode(),
  211.                     'description' => $currency->getDescription(),
  212.                     'base' => $currency->getCode() == $baseCurrency,
  213.                 ];
  214.             }, $xeroCurrencies);
  215.             $xeroOrganisation->setCurrencies($currencies);
  216.             $xeroTrackingCategories $this->fetchTrackingCategories($tenantId);
  217.             $xeroTrackingCategories $xeroTrackingCategories->getTrackingCategories();
  218.             $totalTrackingOptions 0;
  219.             $trackingCategories array_map(function ($category) use (&$totalTrackingOptions) {
  220.                 return [
  221.                     // 'id' => $category->getTrackingCategoryID(),
  222.                     'name' => $category->getName(),
  223.                     'options' => array_map(function ($option) use (&$totalTrackingOptions) {
  224.                         if ($option->getStatus() != 'ACTIVE') return null;
  225.                         $totalTrackingOptions++;
  226.                         return $option->getName();
  227.                         // 'id' => $option->getTrackingOptionID(),
  228.                     }, $category->getOptions()),
  229.                 ];
  230.             }, $xeroTrackingCategories);
  231.             $xeroOrganisation->setTrackingCategories($trackingCategories);
  232.             if ($xeroOrganisation->getId() == null) {
  233.                 $this->em->persist($xeroOrganisation);
  234.             };
  235.             $this->em->flush();
  236.             $response['status'] = 'OK';
  237.             $response['organisation'] = $organization->getName();
  238.             $response['currencies'] = count($currencies);
  239.             $response['trackingCategories'] = $totalTrackingOptions;
  240.         } catch (\Exception $e) {
  241.             $response['status'] = 'ERROR';
  242.             $response['message'] = 'Exception when calling AccountingApi->getCurrencies: ' $e->getMessage() . PHP_EOL;
  243.         }
  244.         return $response;
  245.     }
  246.     public function fetchAccountInfo($id)
  247.     {
  248.         $config \XeroAPI\XeroPHP\Configuration::getDefaultConfiguration()->setAccessToken((string) $this->getSession()->getToken());
  249.         $apiInstance = new \XeroAPI\XeroPHP\Api\AccountingApi(
  250.             new \GuzzleHttp\Client(),
  251.             $config
  252.         );
  253.         if ($this->getHasExpired()) {
  254.             $this->renewToken($this->params);
  255.         }
  256.         $tenantId = (strlen($id) <= 2) ? $this->getTenantId($id) : $id;
  257.         try {
  258.             $result $apiInstance->getAccounts($tenantId);
  259.             return $result;
  260.         } catch (Exception $e) {
  261.             return 'Exception when calling AccountingApi->getAccount: ' $e->getMessage() . PHP_EOL;
  262.             return null;
  263.         }
  264.     }
  265.     public function fetchTrackingCategories($id)
  266.     {
  267.         $config \XeroAPI\XeroPHP\Configuration::getDefaultConfiguration()->setAccessToken((string) $this->getSession()->getToken());
  268.         $apiInstance = new \XeroAPI\XeroPHP\Api\AccountingApi(
  269.             new \GuzzleHttp\Client(),
  270.             $config
  271.         );
  272.         if ($this->getHasExpired()) {
  273.             $this->renewToken($this->params);
  274.         }
  275.         $tenantId = (strlen($id) <= 2) ? $this->getTenantId($id) : $id;
  276.         try {
  277.             $result $apiInstance->getTrackingCategories($tenantId);
  278.             return $result;
  279.         } catch (Exception $e) {
  280.             return 'Exception when calling AccountingApi->getTrackingCategories: ' $e->getMessage() . PHP_EOL;
  281.         }
  282.     }
  283.     public function getXeroTenantId()
  284.     {
  285.         return $this->getSession()->getTenantId();
  286.     }
  287.     public function getIdToken()
  288.     {
  289.         return $this->getSession()->getIdToken();
  290.     }
  291.     public function getHasExpired()
  292.     {
  293.         if ($this->getSession()) {
  294.             if (time() > $this->getExpires()) {
  295.                 return true;
  296.             } else {
  297.                 return false;
  298.             }
  299.         } else {
  300.             return true;
  301.         }
  302.     }
  303.     public function renewToken($params)
  304.     {
  305.         $provider = new \League\OAuth2\Client\Provider\GenericProvider([
  306.             'clientId' => $params->get('xeroClientId'),
  307.             'clientSecret' => $params->get('xeroClientSecret'),
  308.             'redirectUri' => $params->get('xeroRedirectURI'),
  309.             'urlAuthorize' => 'https://login.xero.com/identity/connect/authorize',
  310.             'urlAccessToken' => 'https://identity.xero.com/connect/token',
  311.             'urlResourceOwnerDetails' => 'https://identity.xero.com/resources'
  312.         ]);
  313.         try {
  314.             $newAccessToken $provider->getAccessToken('refresh_token', [
  315.                 'refresh_token' => $this->getRefreshToken()
  316.             ]);
  317.             // Save my token, expiration and refresh token
  318.             $this->setToken(
  319.                 $newAccessToken->getToken(),
  320.                 $newAccessToken->getExpires(),
  321.                 $params->get('xeroTenantId'),
  322.                 $newAccessToken->getRefreshToken(),
  323.                 $newAccessToken->getValues()["id_token"]
  324.             );
  325.             return true;
  326.         } catch (\Exception $e) {
  327.             return false;
  328.         }
  329.     }
  330.     public function stayAlive()
  331.     {
  332.         return $this->fetchInvoices(null"30a11b22-9605-4343-a9e9-6bf7e8e2e4fe");
  333.     }
  334.     /**
  335.      * @throws \GuzzleHttp\Exception\GuzzleException
  336.      */
  337.     public function fetchInvoices($invoice_numbers null$invoice_ids null$tenantId "771ca798-d2cf-4735-ab1b-1449300eda82"$modified_since null$additionalStatus null$returnInvoice false)
  338.     {
  339.         set_time_limit(90);
  340.         // We will automatically mark Vendor Invoices from these Xero Contacts as included
  341.         $arr_include_contacts = [
  342.             "33af4d42-dba2-40a8-8af4-67b835d59569"//IPP World
  343.             "3887d1b2-09fc-4958-aee9-1510ad19bb47"//Clicks2Customers Pte Limited
  344.             "1cac3e9a-e360-4afb-a616-0a1085c63fd9"//Google Asia Pacific Pte. Ltd
  345.             "9e7c2d06-8cf6-4744-a797-61b51e51adf1"//Facebook Ireland Limited
  346.             "fbbbf68a-8931-40d2-a9d0-a6ef55ed296a"//LinkedIn Singapore Pte Ltd
  347.             "3887d1b2-09fc-4958-aee9-1510ad19bb47"//Clicks2Customers Pte Limited
  348.             "d6bf0a80-3d77-4286-a37d-19c9c4d55cd3"//Pinterest Europe Limited
  349.             "11f73751-1ad0-4a47-bc5d-71d316f3cf22"//TikTok Pte. Ltd.
  350.             "a9aa4905-dcdd-43a2-9f36-cc10a6db3e79"//Shutterstock Inc
  351.             "ed6380e1-44f3-4d91-9004-ee81004b49dc"//Adobe Systems Software Ireland Ltd
  352.             "c66e2e0d-69bb-4019-99c0-9d98c40b78e2"//Eyeota Pte Ltd
  353.             "a2ed1311-e992-477b-88d2-87f506a17f4d"//DoubleVerify Inc.
  354.         ];
  355.         // Vendor Invoices from these Xero Contacts will be excluded from the Blotter
  356.         $arr_exclude_contacts = [
  357.             "f47abb0b-c791-47f6-b8a4-129f63905691"//DB Ventures Pte Ltd
  358.             "9c3db7c9-c52b-4e0c-9073-559348fdfa22"//Mediatropy (HK) Limited
  359.             "1b2a8c72-adce-4c38-899f-ed4747ed2cd9"//PT. Mediatropy Bali Digital
  360.             "d976bb37-2931-4ebd-8e07-103d71885733"//CHARM PATH LIMITED
  361.             "e843acd3-8a8d-4e28-b9ca-287fd989e2f8"//Payroll
  362.             "0b91dec9-4104-4b71-9145-2d18ecbb9bab"//Payroll Freelancers SG
  363.             "eb82dddf-162d-48ce-b8ee-f6a679c196f0"//Payroll Bali 
  364.             "94a2eea6-d9b9-4598-a9fb-ba83007ecd6e"//Payroll HK
  365.             "fd24e4f6-bb23-41fd-afcc-956441863768"//the Hive Coworking Space Singapore Pte Ltd
  366.             "d5ed7eb3-4abd-4496-996f-8194a61520ea"//Borderless Hub Pte Ltd
  367.         ];
  368.         if ($this->getHasExpired()) {
  369.             $this->renewToken($this->params);
  370.         }
  371.         $entityManager $this->em;
  372.         $config \XeroAPI\XeroPHP\Configuration::getDefaultConfiguration()->setAccessToken((string) $this->getSession()->getToken());
  373.         $accountingApi = new \XeroAPI\XeroPHP\Api\AccountingApi(
  374.             new \GuzzleHttp\Client(),
  375.             $config
  376.         );
  377.         // $if_modified_since = new \DateTime("2023-01-01T00:00:00.000");
  378.         $if_modified_since $modified_since ? new \DateTime($modified_since) : new \DateTime("-30 days");
  379.         if ($invoice_numbers || $invoice_ids) {
  380.             $if_modified_since = new \DateTime("2020-01-01T00:00:00.000");
  381.         }
  382.         $where null;
  383.         $order null// string
  384.         $contact_ids null// string[] | Filter by a comma-separated list of ContactIDs.
  385.         $statuses = array("PAID""AUTHORISED""SUBMITTED""DRAFT");
  386.         if ($additionalStatus)
  387.             $statuses array_unique(array_merge($statuses$additionalStatus)); // add additional status if needed
  388.         $page 1// int | e.g. page=1 â€“ Up to 100 invoices will be returned in a single API call with line items
  389.         $include_archived null// bool | e.g. includeArchived=true - Contacts with a status of ARCHIVED will be included
  390.         $created_by_my_app null// bool | When set to true you'll only retrieve Invoices created by your app
  391.         $unitdp null// int | e.g. unitdp=4 â€“ You can opt in to use four decimal places for unit amounts
  392.         $stopQuery false;
  393.         $response['inv-add'] = 0;
  394.         $response['inv-update'] = 0;
  395.         $response['inv-not-found'] = 0;
  396.         $response['inv-lineitems'] = 0;
  397.         $response['inv-removed-lineitems'] = 0;
  398.         try {
  399.             $debug "";
  400.             while (!$stopQuery) {
  401.                 $apiResponse $accountingApi->getInvoices($tenantId$if_modified_since$where$order$invoice_ids$invoice_numbers$contact_ids$statuses$page$include_archived$created_by_my_app$unitdp);
  402.                 if (sizeof($apiResponse) < 100) {
  403.                     $stopQuery true;
  404.                 }
  405.                 if (sizeof($apiResponse) == 0) {
  406.                     $response['inv-not-found']++;
  407.                     if($invoice_numbers$stopQuery true;
  408.                     continue;
  409.                 };
  410.                 $vendorInvoiceRepository $this->em->getRepository(VendorInvoice::class);
  411.                 $invoiceRepository $this->em->getRepository(Invoice::class);
  412.                 $xeroLineItemRepository $this->em->getRepository(XeroLineItem::class);
  413.                 $departmentRepository $this->em->getRepository(Department::class);
  414.                 $currencyRepository $this->em->getRepository(Currency::class);
  415.                 $type "";
  416.                 foreach ($apiResponse as $invoice) {
  417.                     $objInv false;
  418.                     if ($invoice['type'] === "ACCPAY") {
  419.                         $type 'vi';
  420.                         $objInv $vendorInvoiceRepository->findOneBy(['invoiceId' => $invoice['invoice_id']]);
  421.                         // INVOICE
  422.                     } elseif ($invoice['type'] === "ACCREC") {
  423.                         $type 'i';
  424.                         $objInv $invoiceRepository->findOneBy(['invoiceId' => $invoice['invoice_id']]);
  425.                     } else {
  426.                         continue;
  427.                     }
  428.                     if ($invoice['invoice_number'] == null) {
  429.                         continue;
  430.                     }
  431.                     if (!$objInv) {
  432.                         // We create a new invoice
  433.                         if ($type === "i") {
  434.                             $objInv = new Invoice();
  435.                         } else {
  436.                             $objInv = new VendorInvoice();
  437.                             $objInv->setBlotter("IGNORED");
  438.                             if (in_array($invoice['contact']['contact_id'], $arr_include_contacts)) {
  439.                                 $objInv->setBlotter("IMPORTED");
  440.                             }
  441.                             if (in_array($invoice['contact']['contact_id'], $arr_exclude_contacts)) {
  442.                                 $objInv->setBlotter("HIDDEN");
  443.                             }
  444.                         }
  445.                         $objInv->setCreatedAt(new \DateTime());
  446.                         $objInv->setDescription('');
  447.                         $response['inv-add']++;
  448.                     } else {
  449.                         $objInv->setUpdatedAt(new \DateTime());
  450.                         $response['inv-update']++;
  451.                     }
  452.                     if (isset($invoice['contact'])) {
  453.                         $objInv->setXeroContactId($invoice['contact']['contact_id']);
  454.                     }
  455.                     $objInv->setInvoiceNo($invoice['invoice_number']);
  456.                     $objInv->setTenantId($tenantId);
  457.                     $currency_code $invoice['currency_code'];
  458.                     $currency $currencyRepository->findOneBy(['iso' => $currency_code]);
  459.                     if (!$currency) {
  460.                         $currency = new Currency();
  461.                         $currency->setIso($currency_code);
  462.                         $currency->setName($currency_code);
  463.                         $currency->setCreatedAt(new \DateTime());
  464.                         $currency->setSymbol('-');
  465.                         $entityManager->persist($currency);
  466.                         $entityManager->flush();
  467.                     }
  468.                     $objInv->setCurrency($currency);
  469.                     // $objInv->setCurrencyRate($invoice['currency_rate']);
  470.                     if (!($objInv instanceof VendorInvoice)) {
  471.                         $objInv->setCurrencyRate($invoice['currency_rate']);
  472.                     }
  473.                     
  474.                     $text_date $invoice['date'];
  475.                     preg_match('#^/Date\((\d{10})#'$text_date$matches);
  476.                     if (array_key_exists(1$matches)) {
  477.                         $dt = new \DateTime('@' $matches[1]);
  478.                     } else {
  479.                         $dt = new \DateTime();
  480.                     }
  481.                     $objInv->setInvoiceDate($dt);
  482.                     $objInv->setTotal($invoice['total']);
  483.                     $current_date = new \DateTime();
  484.                     if ($dt $current_date) {
  485.                         $objInv->setTotalUsd($this->currencyService->convertAtDate(date_format($current_date"Y-m-d"), $currency_code"USD"$invoice['total']));
  486.                         $objInv->setSubTotalUsd($this->currencyService->convertAtDate(date_format($current_date"Y-m-d"), $currency_code"USD"$invoice['sub_total']));
  487.                     } else {
  488.                         $objInv->setTotalUsd($this->currencyService->convertAtDate(date_format($dt"Y-m-d"), $currency_code"USD"$invoice['total']));
  489.                         $objInv->setSubTotalUsd($this->currencyService->convertAtDate(date_format($dt"Y-m-d"), $currency_code"USD"$invoice['sub_total']));
  490.                     }
  491.                     $objInv->setAmountDue($invoice['amount_due']);
  492.                     $objInv->setAmountPaid($invoice['amount_paid']);
  493.                     $objInv->setSubtotal($invoice['sub_total']);
  494.                     $objInv->setReference($invoice['reference']);
  495.                     $objInv->setXeroId($invoice['invoice_id']);
  496.                     $objInv->setInvoiceId($invoice['invoice_id']);
  497.                     $objInv->setXeroStatus($invoice['status']);
  498.                     $text_date2 $invoice['due_date'];
  499.                     preg_match('#^/Date\((\d{10})#'$text_date2$matches2);
  500.                     if (array_key_exists(1$matches2)) {
  501.                         $dt2 = new \DateTime('@' $matches2[1]);
  502.                         $objInv->setDueDate($dt2);
  503.                     }
  504.                     $entityManager->persist($objInv);
  505.                     $entityManager->flush();
  506.                     $entityManager->refresh($objInv);
  507.                     if (method_exists($objInv'getCreator') && $objInv->getCreator() == null) { // INV Creator 
  508.                         $creator $this->fetchInvoiceHistory($objInv->getXeroId());
  509.                         $objInv->setCreator($creator);
  510.                         $entityManager->flush();
  511.                         $entityManager->refresh($objInv);
  512.                     }
  513.                     if ($type == 'i' && $invoice['line_items']) { // INV Line Items
  514.                         $availableLineItems = [];
  515.                         foreach ($invoice['line_items'] as $lineItem) {
  516.                             //if($lineItem['item_code'] == null || !array_key_exists($lineItem['item_code'], $this->arr_item_codes)) continue; 
  517.                             // if(!array_key_exists($lineItem['item_code'], $arr_item_codes)) continue;    
  518.                             if ($lineItem['item_code'] != null && !array_key_exists(strtolower($lineItem['item_code']), $this->arr_item_codes))
  519.                                 continue;
  520.                             if ($lineItem['item_code'] == null && !preg_match($this->projectIdPattern$lineItem['description']) && ($lineItem['line_amount'] == null || $lineItem['line_amount'] == 0))
  521.                                 continue;
  522.                             $lineItemId $lineItem['line_item_id'];
  523.                             $lineItemTotal $lineItem['line_amount'] + $lineItem['tax_amount'];
  524.                             $lineItemSubTotal $lineItem['line_amount'];
  525.                             $availableLineItems[] = $lineItemId;
  526.                             $objLineItem $xeroLineItemRepository->findOneBy(['xeroId' => $lineItemId]) ?? new XeroLineItem();
  527.                             if ($objLineItem->getId() == null) {
  528.                                 $objLineItem->setCreatedAt(new \DateTimeImmutable());
  529.                             } else {
  530.                                 $objLineItem->setUpdatedAt(new \DateTimeImmutable());
  531.                             }
  532.                             if ($lineItem['item_code'] == null) {
  533.                                 $lineItemDepartment null// default blank department
  534.                                 $itemCode null// default blank item code
  535.                                 $isIncluded false;
  536.                             } else {
  537.                                 $lineItemDepartment $departmentRepository->findOneBy(['id' => $this->arr_item_codes[strtolower($lineItem['item_code'])]]) ?? null;
  538.                                 $isIncluded = !in_array(strtolower($lineItem['item_code']), $this->arr_excluded_item_codes);
  539.                                 $itemCode $lineItem['item_code'];
  540.                             };
  541.                             $objLineItem->setParentNo($objInv->getInvoiceNo());
  542.                             if (method_exists($objInv'getCreator'))
  543.                                 $objLineItem->setInvoice($objInv);
  544.                             $objLineItem->setXeroId($lineItemId);
  545.                             $objLineItem->setTenantId($tenantId);
  546.                             $objLineItem->setIsIncluded($isIncluded);
  547.                             $objLineItem->setDepartment($lineItemDepartment);
  548.                             $objLineItem->setDescription($lineItem['description']);
  549.                             $objLineItem->setQuantity($lineItem['quantity']);
  550.                             $objLineItem->setUnitAmount($lineItem['unit_amount']);
  551.                             $objLineItem->setItemCode($itemCode);
  552.                             $objLineItem->setAccountCode($lineItem['account_code']);
  553.                             $objLineItem->setAccountId($lineItem['account_id']);
  554.                             $objLineItem->setTotal($lineItemTotal);
  555.                             $objLineItem->setSubtotal($lineItemSubTotal);
  556.                             if ($dt $current_date) {
  557.                                 $objLineItem->setTotalUsd($this->currencyService->convertAtDate(date_format($current_date"Y-m-d"), $currency_code"USD"$lineItemTotal));
  558.                                 $objLineItem->setSubTotalUsd($this->currencyService->convertAtDate(date_format($current_date"Y-m-d"), $currency_code"USD"$lineItemSubTotal));
  559.                             } else {
  560.                                 $objLineItem->setTotalUsd($this->currencyService->convertAtDate(date_format($dt"Y-m-d"), $currency_code"USD"$lineItemTotal));
  561.                                 $objLineItem->setSubTotalUsd($this->currencyService->convertAtDate(date_format($dt"Y-m-d"), $currency_code"USD"$lineItemSubTotal));
  562.                             }
  563.                             $entityManager->persist($objLineItem);
  564.                             $entityManager->flush();
  565.                             $response['inv-lineitems']++;
  566.                         }
  567.                         $entityManager->refresh($objInv);
  568.                         foreach ($objInv->getXeroLineItems() as $lineItem) {
  569.                             if (in_array($lineItem->getXeroId(), $availableLineItems))
  570.                                 continue;
  571.                             $entityManager->remove($lineItem);
  572.                             $entityManager->flush();
  573.                             $response['inv-removed-lineitems']++;
  574.                         }
  575.                     }
  576.                     // $this->projectService->projectXeroAllocationEmail($objInv);
  577.                     if ($returnInvoice && ($invoice_ids || $invoice_numbers)) {
  578.                         return $objInv;
  579.                     }
  580.                 }
  581.                 //
  582.                 $page++;
  583.                 //
  584.             }
  585.         } catch (\Exception $exception) {
  586.             // dd($exception);
  587.             $response['status'] = 'ERROR';
  588.             $response['message'] = 'API exception: ' $exception->getMessage();
  589.             return $response;
  590.         }
  591.         $response['status'] = 'OK';
  592.         return $response;
  593.     }
  594.     public function fetchSalesOrderHistory($soId)
  595.     {
  596.         $config \XeroAPI\XeroPHP\Configuration::getDefaultConfiguration()->setAccessToken((string) $this->getSession()->getToken());
  597.         $accountingApi = new \XeroAPI\XeroPHP\Api\AccountingApi(
  598.             new \GuzzleHttp\Client(),
  599.             $config
  600.         );
  601.         if ($this->getHasExpired()) {
  602.             $this->renewToken($this->params);
  603.         }
  604.         $apiResponse $accountingApi->getQuoteHistory($this->params->get('xeroTenantId'), $soId);
  605.         if (sizeof($apiResponse) > 0) {
  606.             return $apiResponse[sizeof($apiResponse) - 1]['user'];
  607.         } else {
  608.             return "";
  609.         }
  610.     }
  611.     public function fetchInvoiceHistory($invId)
  612.     {
  613.         $config \XeroAPI\XeroPHP\Configuration::getDefaultConfiguration()->setAccessToken((string) $this->getSession()->getToken());
  614.         $accountingApi = new \XeroAPI\XeroPHP\Api\AccountingApi(
  615.             new \GuzzleHttp\Client(),
  616.             $config
  617.         );
  618.         if ($this->getHasExpired()) {
  619.             $this->renewToken($this->params);
  620.         }
  621.         $apiResponse $accountingApi->getInvoiceHistory($this->params->get('xeroTenantId'), $invId);
  622.         if (sizeof($apiResponse) > 0) {
  623.             return $apiResponse[sizeof($apiResponse) - 1]['user'];
  624.         } else {
  625.             return "";
  626.         }
  627.     }
  628.     public function fetchSoCreators()
  629.     {
  630.         $salesOrderRepository $this->em->getRepository(SalesOrder::class);
  631.         $allSo $salesOrderRepository->findBy(array('creator' => NULL));
  632.         $i 0;
  633.         foreach ($allSo as $so) {
  634.             $creator $this->fetchSalesOrderHistory($so->getXeroId());
  635.             $so->setCreator($creator);
  636.             $this->em->persist($so);
  637.             $i++;
  638.             if ($i 50) {
  639.                 break;
  640.             }
  641.         }
  642.         $this->em->flush();
  643.         return $i;
  644.     }
  645.     public function fetchInvCreators()
  646.     {
  647.         $salesOrderRepository $this->em->getRepository(Invoice::class);
  648.         $allInv $salesOrderRepository->findBy(array('creator' => NULL));
  649.         $i 0;
  650.         foreach ($allInv as $inv) {
  651.             $creator $this->fetchInvoiceHistory($inv->getXeroId());
  652.             $inv->setCreator($creator);
  653.             $this->em->persist($inv);
  654.             $i++;
  655.             if ($i 50) {
  656.                 break;
  657.             }
  658.         }
  659.         $this->em->flush();
  660.         return $i;
  661.     }
  662.     public function fetchCreditNotes($cnNumbers null$tenantId "771ca798-d2cf-4735-ab1b-1449300eda82"$date null)
  663.     {
  664.         if ($this->getHasExpired()) {
  665.             $this->renewToken($this->params);
  666.         }
  667.         $entityManager $this->em;
  668.         $config \XeroAPI\XeroPHP\Configuration::getDefaultConfiguration()->setAccessToken((string) $this->getSession()->getToken());
  669.         $accountingApi = new \XeroAPI\XeroPHP\Api\AccountingApi(
  670.             new \GuzzleHttp\Client(),
  671.             $config
  672.         );
  673.         $order null// string
  674.         $contact_ids null// string[] | Filter by a comma-separated list of ContactIDs.
  675.         $statuses null//array("SUBMITTED", "PAID", "AUTHORISED", "VOIDED", "DRAFT", "DELETED");
  676.         $page 1// int | e.g. page=1 â€“ Up to 100 invoices will be returned in a single API call with line items
  677.         $dateFrom $date == null ? new \DateTime("2022-01-01") : new \DateTime($date);
  678.         $allApiResponses = [];
  679.         $stopQuery false;
  680.         $where null;
  681.         $unitdp 4;
  682.         $response['total_add'] = 0;
  683.         $response['total_update'] = 0;
  684.         $response['total_lineitem_add'] = 0;
  685.         $response['total_lineitem_update'] = 0;
  686.         $response['total_allocation_add'] = 0;
  687.         $response['total_allocation_remove'] = 0;
  688.         $xeroCreditNoteRepository $entityManager->getRepository(XeroCreditNote::class);
  689.         $xeroLineItemRepository $entityManager->getRepository(XeroLineItem::class);
  690.         $xeroCreditNoteAllocationRepositorty $entityManager->getRepository(XeroCreditNoteAllocation::class);
  691.         $invoiceRepository $entityManager->getRepository(Invoice::class);
  692.         $departmentRepository $entityManager->getRepository(Department::class);
  693.         $currencyRepository $entityManager->getRepository(Currency::class);
  694.         try {
  695.             //todo this is needed but maybe need to rate limit somehow
  696.             while (!$stopQuery) {
  697.                 $apiResponse $accountingApi->getCreditNotes($this->params->get('xeroTenantId'), $dateFrom$where$order$page$unitdp);
  698.                 if (sizeof($apiResponse) < 100) {
  699.                     $stopQuery true;
  700.                 }
  701.                 $page++;
  702.                 foreach ($apiResponse as $cn) {
  703.                     if ($cn['status'] == 'DELETED')
  704.                         continue;
  705.                     $objCn $xeroCreditNoteRepository->findOneBy(['creditNoteId' => $cn['credit_note_id']]);
  706.                     if (!$objCn) {
  707.                         $objCn = new XeroCreditNote();
  708.                         $objCn->setCreatedAt(new \DateTimeImmutable());
  709.                         $response['total_add']++;
  710.                     } else {
  711.                         $objCn->setUpdatedAt(new \DateTimeImmutable());
  712.                         $response['total_update']++;
  713.                     }
  714.                     $objCn->setCreditNoteId($cn['credit_note_id']);
  715.                     $objCn->setCreditNoteNo($cn['credit_note_number']);
  716.                     $objCn->setXeroStatus($cn['status']);
  717.                     $objCn->setReference($cn['reference']);
  718.                     $objCn->setXeroContactId($cn['contact']['contact_id']);
  719.                     $currency_code $cn['currency_code'];
  720.                     $currency $currencyRepository->findOneBy(['iso' => $currency_code]);
  721.                     if (!$currency) {
  722.                         $currency = new Currency();
  723.                         $currency->setIso($currency_code);
  724.                         $currency->setName($currency_code);
  725.                         $currency->setCreatedAt(new \DateTime());
  726.                         $currency->setSymbol('-');
  727.                         $entityManager->persist($currency);
  728.                         $entityManager->flush();
  729.                     }
  730.                     $objCn->setCurrency($currency);
  731.                     $text_date $cn['date'];
  732.                     preg_match('#^/Date\((\d{10})#'$text_date$matches);
  733.                     if (array_key_exists(1$matches)) {
  734.                         $dt = new \DateTimeImmutable('@' $matches[1]);
  735.                     } else {
  736.                         $dt = new \DateTimeImmutable();
  737.                     }
  738.                     $objCn->setCreditNoteDate($dt);
  739.                     $objCn->setTotal($cn['total']);
  740.                     $objCn->setSubTotal($cn['sub_total']);
  741.                     $objCn->setRemainingCredit($cn['remaining_credit']);
  742.                     $current_date = new \DateTime();
  743.                     if ($dt $current_date) {
  744.                         $objCn->setTotalUsd($this->currencyService->convertAtDate(date_format($current_date"Y-m-d"), $currency_code"USD"$cn['total']));
  745.                         $objCn->setSubTotalUsd($this->currencyService->convertAtDate(date_format($current_date"Y-m-d"), $currency_code"USD"$cn['sub_total']));
  746.                         $objCn->setRemainingCreditUsd($this->currencyService->convertAtDate(date_format($current_date"Y-m-d"), $currency_code"USD"$cn['remaining_credit']));
  747.                     } else {
  748.                         $objCn->setTotalUsd($this->currencyService->convertAtDate(date_format($dt"Y-m-d"), $currency_code"USD"$cn['total']));
  749.                         $objCn->setSubTotalUsd($this->currencyService->convertAtDate(date_format($dt"Y-m-d"), $currency_code"USD"$cn['sub_total']));
  750.                         $objCn->setRemainingCreditUsd($this->currencyService->convertAtDate(date_format($dt"Y-m-d"), $currency_code"USD"$cn['remaining_credit']));
  751.                     }
  752.                     if ($cn['line_items']) {
  753.                         $availableLineItems = [];
  754.                         foreach ($cn['line_items'] as $lineItem) {
  755.                             if ($lineItem['item_code'] != null && !array_key_exists(strtolower($lineItem['item_code']), $this->arr_item_codes))
  756.                                 continue;
  757.                             if ($lineItem['item_code'] == null && ($lineItem['line_amount'] == null || $lineItem['line_amount'] == 0))
  758.                                 continue;
  759.                             $lineItemId $lineItem['line_item_id'];
  760.                             $lineItemTotal $lineItem['line_amount'] + $lineItem['tax_amount'];
  761.                             $lineItemSubTotal $lineItem['line_amount'];
  762.                             $availableLineItems[] = $lineItemId;
  763.                             $objLineItem $xeroLineItemRepository->findOneBy(['xeroId' => $lineItemId]) ?? new XeroLineItem();
  764.                             if ($objLineItem->getId() == null) {
  765.                                 $objLineItem->setCreatedAt(new \DateTimeImmutable());
  766.                             } else {
  767.                                 $objLineItem->setUpdatedAt(new \DateTimeImmutable());
  768.                             }
  769.                             // if($lineItem['item_code'] != null && array_key_exists(strtolower($lineItem['item_code'], $this->arr_item_codes)){
  770.                             if ($lineItem['item_code'] == null) {
  771.                                 // $lineItemDepartment = $departmentRepository->findOneBy(['id' => $this->arr_item_codes[strtolower("Discount")]]) ?? null; // default blank department
  772.                                 // $itemCode = "Discount"; // default blank item code
  773.                                 // $isIncluded = true;
  774.                                 $lineItemDepartment null// default blank department
  775.                                 $itemCode null// default blank item code
  776.                                 $isIncluded false;
  777.                             } else {
  778.                                 $lineItemDepartment $departmentRepository->findOneBy(['id' => $this->arr_item_codes[strtolower($lineItem['item_code'])]]) ?? null;
  779.                                 $isIncluded = !in_array(strtolower($lineItem['item_code']), $this->arr_excluded_item_codes);
  780.                                 $itemCode $lineItem['item_code'];
  781.                             };
  782.                             $objLineItem->setParentNo($objCn->getCreditNoteNo());
  783.                             $objLineItem->setCreditNote($objCn);
  784.                             $objLineItem->setXeroId($lineItemId);
  785.                             $objLineItem->setTenantId($tenantId);
  786.                             $objLineItem->setIsIncluded($isIncluded);
  787.                             $objLineItem->setDepartment($lineItemDepartment);
  788.                             $objLineItem->setDescription($lineItem['description']);
  789.                             $objLineItem->setQuantity($lineItem['quantity']);
  790.                             $objLineItem->setUnitAmount($lineItem['unit_amount']);
  791.                             $objLineItem->setItemCode($itemCode);
  792.                             $objLineItem->setAccountCode($lineItem['account_code']);
  793.                             $objLineItem->setAccountId($lineItem['account_id']);
  794.                             $objLineItem->setTotal($lineItemTotal);
  795.                             $objLineItem->setSubtotal($lineItemSubTotal);
  796.                             $objLineItem->setTotalUsd($this->currencyService->convertAtDate(date_format($objCn->getCreditNoteDate(), "Y-m-d"), $objCn->getCurrency()->getIso(), "USD"$lineItemTotal));
  797.                             $objLineItem->setSubTotalUsd($this->currencyService->convertAtDate(date_format($objCn->getCreditNoteDate(), "Y-m-d"), $objCn->getCurrency()->getIso(), "USD"$lineItemSubTotal));
  798.                             $entityManager->persist($objLineItem);
  799.                             $entityManager->flush();
  800.                             $response['total_lineitem_add']++;
  801.                         }
  802.                         $entityManager->refresh($objLineItem);
  803.                         foreach ($objCn->getXeroLineItems() as $lineItem) {
  804.                             if (in_array($lineItem->getXeroId(), $availableLineItems))
  805.                                 continue;
  806.                             $entityManager->remove($lineItem);
  807.                             $entityManager->flush();
  808.                             $response['inv-removed-lineitems']++;
  809.                         }
  810.                     }
  811.                     foreach ($cn['allocations'] as $allocation) {
  812.                         $objInv $invoiceRepository->findOneBy(['invoiceId' => $allocation['invoice']['invoice_id']]);
  813.                         if ($objInv == null)
  814.                             continue;
  815.                         $objAllocation $xeroCreditNoteAllocationRepositorty->findOneBy(['xeroCreditNote' => $objCn'invoice' => $objInv]) ?? new XeroCreditNoteAllocation();
  816.                         $objAllocation->setXeroCreditNote($objCn);
  817.                         $objAllocation->setInvoice($objInv);
  818.                         $ratio $allocation['amount'] / $cn['total'];
  819.                         $amountAllocated $objCn->getSubTotal() * $ratio;
  820.                         $amountAllocatedUsd $objCn->getSubTotalUsd() * $ratio;
  821.                         $objAllocation->setAmount($amountAllocated);
  822.                         $objAllocation->setAmountUsd($amountAllocatedUsd);
  823.                         $response['total_allocation_add']++;
  824.                         $entityManager->persist($objAllocation);
  825.                         $entityManager->flush();
  826.                     }
  827.                     $entityManager->persist($objCn);
  828.                     $entityManager->flush();
  829.                     $entityManager->refresh($objCn);
  830.                     if ($objCn->getXeroCreditNoteAllocation()) {
  831.                         foreach ($objCn->getXeroCreditNoteAllocation() as $objAllocation) {
  832.                             $found false;
  833.                             foreach ($cn['allocations'] as $allocation) {
  834.                                 if ($allocation['invoice']['invoice_id'] == $allocation->getInvoiceId()) {
  835.                                     $found true;
  836.                                     break;
  837.                                 }
  838.                             }
  839.                             if (!$found) {
  840.                                 $entityManager->remove($objAllocation);
  841.                                 $response['total_allocation_remove']++;
  842.                             }
  843.                         }
  844.                     }
  845.                 }
  846.             }
  847.         } catch (\Exception $exception) {
  848.             $response['status'] = 'ERROR';
  849.             $response['message'] = 'API exception: ' $exception->getMessage();
  850.             return $response;
  851.         }
  852.         $response['status'] = 'OK';
  853.         return $response;
  854.     }
  855.     public function populateLineItem($type null$limit 99)
  856.     {
  857.         if ($this->getHasExpired()) {
  858.             $this->renewToken($this->params);
  859.         }
  860.         $config \XeroAPI\XeroPHP\Configuration::getDefaultConfiguration()->setAccessToken((string) $this->getSession()->getToken());
  861.         $accountingApi = new \XeroAPI\XeroPHP\Api\AccountingApi(
  862.             new \GuzzleHttp\Client(),
  863.             $config
  864.         );
  865.         $tenantId "771ca798-d2cf-4735-ab1b-1449300eda82";
  866.         $if_modified_since null;
  867.         $where null;
  868.         $order null// string
  869.         $contact_ids null// string[] | Filter by a comma-separated list of ContactIDs.
  870.         $page 1// int | e.g. page=1 â€“ Up to 100 invoices will be returned in a single API call with line items
  871.         $include_archived null// bool | e.g. includeArchived=true - Contacts with a status of ARCHIVED will be included
  872.         $created_by_my_app null// bool | When set to true you'll only retrieve Invoices created by your app
  873.         $unitdp null// int | e.g. unitdp=4 â€“ You can opt in to use four decimal places for unit amounts
  874.         $entityManager $this->em;
  875.         $xeroLineItemRepository $entityManager->getRepository(XeroLineItem::class);
  876.         $departmentRepository $entityManager->getRepository(Department::class);
  877.         if ($type == 'inv' || $type == null) {
  878.             $invoice_numbers null;
  879.             $invoice_ids null;
  880.             $statuses = array("PAID""AUTHORISED""SUBMITTED""DRAFT");
  881.             $invoiceRepository $entityManager->getRepository(Invoice::class);
  882.             $allObjInv $invoiceRepository->findAllByEmptyLineItems($limit);
  883.             foreach ($allObjInv as $objInv) {
  884.                 if ($objInv->getXeroId() == null || $objInv->getXeroId() == 'china')
  885.                     continue;
  886.                 $invoice_ids $objInv->getXeroId();
  887.                 $tenantId $objInv->getTenantId();
  888.                 try {
  889.                     $apiResponse $accountingApi->getInvoices($tenantId$if_modified_since$where$order$invoice_ids$invoice_numbers$contact_ids$statuses$page$include_archived$created_by_my_app$unitdp);
  890.                     foreach ($apiResponse as $invoice) {
  891.                         if ($invoice['line_items']) {
  892.                             foreach ($invoice['line_items'] as $lineItem) {
  893.                                 if ($lineItem['item_code'] != null && !array_key_exists(strtolower($lineItem['item_code']), $this->arr_item_codes))
  894.                                     continue;
  895.                                 if ($lineItem['item_code'] == null && ($lineItem['line_amount'] == null || $lineItem['line_amount'] == 0))
  896.                                     continue;
  897.                                 $lineItemId $lineItem['line_item_id'];
  898.                                 $lineItemTotal $lineItem['line_amount'] + $lineItem['tax_amount'];
  899.                                 $lineItemSubTotal $lineItem['line_amount'];
  900.                                 $objLineItem $xeroLineItemRepository->findOneBy(['xeroId' => $lineItemId]) ?? new XeroLineItem();
  901.                                 if ($objLineItem->getId() == null) {
  902.                                     $objLineItem->setCreatedAt(new \DateTimeImmutable());
  903.                                 } else {
  904.                                     $objLineItem->setUpdatedAt(new \DateTimeImmutable());
  905.                                 }
  906.                                 // if($lineItem['item_code'] != null && array_key_exists(strtolower($lineItem['item_code'], $this->arr_item_codes)){
  907.                                 if ($lineItem['item_code'] == null) {
  908.                                     // $lineItemDepartment = $departmentRepository->findOneBy(['id' => $this->arr_item_codes[strtolower("Discount")]]) ?? null; // default blank department
  909.                                     // $itemCode = "Discount"; // default blank item code
  910.                                     // $isIncluded = true;
  911.                                     $lineItemDepartment null// default blank department
  912.                                     $itemCode null// default blank item code
  913.                                     $isIncluded false;
  914.                                 } else {
  915.                                     $lineItemDepartment $departmentRepository->findOneBy(['id' => $this->arr_item_codes[strtolower($lineItem['item_code'])]]) ?? null;
  916.                                     $isIncluded = !in_array(strtolower($lineItem['item_code']), $this->arr_excluded_item_codes);
  917.                                     $itemCode $lineItem['item_code'];
  918.                                 };
  919.                                 $objLineItem->setParentNo($objInv->getInvoiceNo());
  920.                                 $objLineItem->setInvoice($objInv);
  921.                                 $objLineItem->setXeroId($lineItemId);
  922.                                 $objLineItem->setTenantId($tenantId);
  923.                                 $objLineItem->setIsIncluded($isIncluded);
  924.                                 $objLineItem->setDepartment($lineItemDepartment);
  925.                                 $objLineItem->setDescription($lineItem['description']);
  926.                                 $objLineItem->setQuantity($lineItem['quantity']);
  927.                                 $objLineItem->setUnitAmount($lineItem['unit_amount']);
  928.                                 $objLineItem->setItemCode($itemCode);
  929.                                 $objLineItem->setAccountCode($lineItem['account_code']);
  930.                                 $objLineItem->setAccountId($lineItem['account_id']);
  931.                                 $objLineItem->setTotal($lineItemTotal);
  932.                                 $objLineItem->setSubtotal($lineItemSubTotal);
  933.                                 $objLineItem->setTotalUsd($this->currencyService->convertAtDate(date_format($objInv->getInvoiceDate(), "Y-m-d"), $objInv->getCurrency()->getIso(), "USD"$lineItemTotal));
  934.                                 $objLineItem->setSubTotalUsd($this->currencyService->convertAtDate(date_format($objInv->getInvoiceDate(), "Y-m-d"), $objInv->getCurrency()->getIso(), "USD"$lineItemSubTotal));
  935.                                 $entityManager->persist($objLineItem);
  936.                                 $entityManager->flush();
  937.                             }
  938.                             $response['inv-success'][] = $objInv->getInvoiceNo();
  939.                         }
  940.                     }
  941.                     // $page++;  
  942.                 } catch (\Exception $exception) {
  943.                     // dd($exception);
  944.                     $response['inv-status'] = 'ERROR';
  945.                     $response['inv-message'] = 'API exception: ' $exception->getMessage();
  946.                     // return $result;
  947.                     // $response['error'] = $invoice_ids.' ['.$exception->getMessage().']';
  948.                     // return $response;
  949.                 }
  950.             };
  951.         }
  952.         if ($type == 'so' || $type == null) {
  953.             $soNumbers null;
  954.             $statuses null;
  955.             $salesOrderRepository $entityManager->getRepository(SalesOrder::class);
  956.             $allObjSo $salesOrderRepository->findAllByEmptyLineItems($limit);
  957.             foreach ($allObjSo as $objSo) {
  958.                 $soNumbers $objSo->getSalesOrderNo();
  959.                 $tenantId $objSo->getTenantId();
  960.                 try {
  961.                     $apiResponse $accountingApi->getQuotes($tenantIdnullnullnullnullnull$contact_ids$statuses$page$order$soNumbers);
  962.                     foreach ($apiResponse as $so) {
  963.                         if ($so['line_items']) {
  964.                             foreach ($so['line_items'] as $lineItem) {
  965.                                 // if($lineItem['item_code'] == null || !array_key_exists($lineItem['item_code'], $this->arr_item_codes)) continue;    
  966.                                 if ($lineItem['item_code'] != null && !array_key_exists(strtolower($lineItem['item_code']), $this->arr_item_codes))
  967.                                     continue;
  968.                                 if ($lineItem['item_code'] == null && ($lineItem['line_amount'] == null || $lineItem['line_amount'] == 0))
  969.                                     continue;
  970.                                 $lineItemId $lineItem['line_item_id'];
  971.                                 $lineItemTotal $lineItem['line_amount'] + $lineItem['tax_amount'];
  972.                                 $lineItemSubTotal $lineItem['line_amount'];
  973.                                 $objLineItem $xeroLineItemRepository->findOneBy(['xeroId' => $lineItemId]) ?? new XeroLineItem();
  974.                                 if ($objLineItem->getId() == null) {
  975.                                     $objLineItem->setCreatedAt(new \DateTimeImmutable());
  976.                                 } else {
  977.                                     $objLineItem->setUpdatedAt(new \DateTimeImmutable());
  978.                                 }
  979.                                 if ($lineItem['item_code'] == null) {
  980.                                     $lineItemDepartment null// default blank department
  981.                                     $itemCode null// default blank item code
  982.                                     $isIncluded false;
  983.                                 } else {
  984.                                     $lineItemDepartment $departmentRepository->findOneBy(['id' => $this->arr_item_codes[strtolower($lineItem['item_code'])]]) ?? null;
  985.                                     $isIncluded = !in_array(strtolower($lineItem['item_code']), $this->arr_excluded_item_codes);
  986.                                     $itemCode $lineItem['item_code'];
  987.                                 };
  988.                                 $objLineItem->setParentNo($soNumbers);
  989.                                 $objLineItem->setSalesOrder($objSo);
  990.                                 $objLineItem->setXeroId($lineItemId);
  991.                                 $objLineItem->setTenantId($tenantId);
  992.                                 $objLineItem->setIsIncluded($isIncluded);
  993.                                 $objLineItem->setDepartment($lineItemDepartment);
  994.                                 $objLineItem->setDescription($lineItem['description']);
  995.                                 $objLineItem->setQuantity($lineItem['quantity']);
  996.                                 $objLineItem->setUnitAmount($lineItem['unit_amount']);
  997.                                 $objLineItem->setItemCode($itemCode);
  998.                                 $objLineItem->setAccountCode($lineItem['account_code']);
  999.                                 $objLineItem->setAccountId($lineItem['account_id']);
  1000.                                 $objLineItem->setTotal($lineItemTotal);
  1001.                                 $objLineItem->setSubtotal($lineItemSubTotal);
  1002.                                 $objLineItem->setTotalUsd($this->currencyService->convertAtDate(date_format($objSo->getQuoteDate(), "Y-m-d"), $objSo->getCurrency()->getIso(), "USD"$lineItemTotal));
  1003.                                 $objLineItem->setSubTotalUsd($this->currencyService->convertAtDate(date_format($objSo->getQuoteDate(), "Y-m-d"), $objSo->getCurrency()->getIso(), "USD"$lineItemSubTotal));
  1004.                                 $entityManager->persist($objLineItem);
  1005.                                 $entityManager->flush();
  1006.                             }
  1007.                             $response['so-success'][] = $soNumbers;
  1008.                         }
  1009.                     }
  1010.                     // $page++;  
  1011.                 } catch (\Exception $exception) {
  1012.                     // dd($exception);
  1013.                     $response['so-status'] = 'ERROR';
  1014.                     $response['so-message'] = 'API exception: ' $exception->getMessage();
  1015.                     // return $result;
  1016.                     // $response['error'] = $invoice_ids.' ['.$exception->getMessage().']';
  1017.                     // return $response;
  1018.                 }
  1019.             };
  1020.         }
  1021.         $response['status'] = 'OK';
  1022.         return $response;
  1023.     }
  1024.     public function populateCreditNote($datetime$limit 99)
  1025.     {
  1026.         if ($this->getHasExpired()) {
  1027.             $this->renewToken($this->params);
  1028.         }
  1029.         $config \XeroAPI\XeroPHP\Configuration::getDefaultConfiguration()->setAccessToken((string) $this->getSession()->getToken());
  1030.         $accountingApi = new \XeroAPI\XeroPHP\Api\AccountingApi(
  1031.             new \GuzzleHttp\Client(),
  1032.             $config
  1033.         );
  1034.         $entityManager $this->em;
  1035.         $invoiceRepository $entityManager->getRepository(Invoice::class);
  1036.         $xeroLineItemRepository $entityManager->getRepository(XeroLineItem::class);
  1037.         $departmentRepository $entityManager->getRepository(Department::class);
  1038.         $invoiceRepository $entityManager->getRepository(Invoice::class);
  1039.         $allObjInv $invoiceRepository->findAllByEmptyCreditNotes($datetime$limit);
  1040.         $response = [];
  1041.         $response['has-credit-notes'] = 0;
  1042.         $response['no-credit-notes'] = 0;
  1043.         foreach ($allObjInv as $objInv) {
  1044.             if ($objInv->getXeroId() == null || $objInv->getXeroId() == 'china')
  1045.                 continue;
  1046.             $invoice_ids $objInv->getXeroId();
  1047.             $tenantId $objInv->getTenantId();
  1048.             try {
  1049.                 $apiResponse $accountingApi->getInvoices($tenantIdnullnullnull$invoice_idsnullnullnullnullnullnullnull);
  1050.                 foreach ($apiResponse as $invoice) {
  1051.                     if ($invoice['credit_notes']) {
  1052.                         $response['has-credit-notes']++;
  1053.                         continue;
  1054.                         foreach ($invoice['line_items'] as $lineItem) {
  1055.                             if ($lineItem['item_code'] != null && !array_key_exists(strtolower($lineItem['item_code']), $this->arr_item_codes))
  1056.                                 continue;
  1057.                             if ($lineItem['item_code'] == null && ($lineItem['line_amount'] == null || $lineItem['line_amount'] == 0))
  1058.                                 continue;
  1059.                             $lineItemId $lineItem['line_item_id'];
  1060.                             $lineItemTotal $lineItem['line_amount'] + $lineItem['tax_amount'];
  1061.                             $lineItemSubTotal $lineItem['line_amount'];
  1062.                             $objLineItem $xeroLineItemRepository->findOneBy(['xeroId' => $lineItemId]) ?? new XeroLineItem();
  1063.                             if ($objLineItem->getId() == null) {
  1064.                                 $objLineItem->setCreatedAt(new \DateTimeImmutable());
  1065.                             } else {
  1066.                                 $objLineItem->setUpdatedAt(new \DateTimeImmutable());
  1067.                             }
  1068.                             if ($lineItem['item_code'] == null) {
  1069.                                 $lineItemDepartment null// default blank department
  1070.                                 $itemCode null// default blank item code
  1071.                                 $isIncluded false;
  1072.                             } else {
  1073.                                 $lineItemDepartment $departmentRepository->findOneBy(['id' => $this->arr_item_codes[strtolower($lineItem['item_code'])]]) ?? null;
  1074.                                 $isIncluded = !in_array(strtolower($lineItem['item_code']), $this->arr_excluded_item_codes);
  1075.                                 $itemCode $lineItem['item_code'];
  1076.                             };
  1077.                             $objLineItem->setParentNo($objInv->getInvoiceNo());
  1078.                             $objLineItem->setInvoice($objInv);
  1079.                             $objLineItem->setXeroId($lineItemId);
  1080.                             $objLineItem->setTenantId($tenantId);
  1081.                             $objLineItem->setIsIncluded($isIncluded);
  1082.                             $objLineItem->setDepartment($lineItemDepartment);
  1083.                             $objLineItem->setDescription($lineItem['description']);
  1084.                             $objLineItem->setQuantity($lineItem['quantity']);
  1085.                             $objLineItem->setUnitAmount($lineItem['unit_amount']);
  1086.                             $objLineItem->setItemCode($itemCode);
  1087.                             $objLineItem->setAccountCode($lineItem['account_code']);
  1088.                             $objLineItem->setAccountId($lineItem['account_id']);
  1089.                             $objLineItem->setTotal($lineItemTotal);
  1090.                             $objLineItem->setSubtotal($lineItemSubTotal);
  1091.                             $objLineItem->setTotalUsd($this->currencyService->convertAtDate(date_format($objInv->getInvoiceDate(), "Y-m-d"), $objInv->getCurrency()->getIso(), "USD"$lineItemTotal));
  1092.                             $objLineItem->setSubTotalUsd($this->currencyService->convertAtDate(date_format($objInv->getInvoiceDate(), "Y-m-d"), $objInv->getCurrency()->getIso(), "USD"$lineItemSubTotal));
  1093.                             $entityManager->persist($objLineItem);
  1094.                             $entityManager->flush();
  1095.                         }
  1096.                         $response['inv-success'][] = $objInv->getInvoiceNo();
  1097.                     } else {
  1098.                         $response['no-credit-notes']++;
  1099.                         $objInv->setUpdatedAt(new \DateTime());
  1100.                         $entityManager->flush();
  1101.                     }
  1102.                 }
  1103.             } catch (\Exception $exception) {
  1104.                 $response['status'] = 'ERROR';
  1105.                 $response['message'] = 'API exception: ' $exception->getMessage();
  1106.             }
  1107.         };
  1108.         $response['status'] = 'OK';
  1109.         return $response;
  1110.     }
  1111.     public function checkStatus($type null$datetime null$limit 99)
  1112.     {
  1113.         if ($this->getHasExpired()) {
  1114.             $this->renewToken($this->params);
  1115.         }
  1116.         $config \XeroAPI\XeroPHP\Configuration::getDefaultConfiguration()->setAccessToken((string) $this->getSession()->getToken());
  1117.         $accountingApi = new \XeroAPI\XeroPHP\Api\AccountingApi(
  1118.             new \GuzzleHttp\Client(),
  1119.             $config
  1120.         );
  1121.         $entityManager $this->em;
  1122.         $response = [];
  1123.         if ($type == 'inv' || $type == null) {
  1124.             $invoice_ids null;
  1125.             $invoiceRepository $entityManager->getRepository(Invoice::class);
  1126.             $allObjInv $invoiceRepository->findAllUpdatedBefore($datetime$limit);
  1127.             foreach ($allObjInv as $objInv) {
  1128.                 if ($objInv->getXeroId() == null || $objInv->getXeroId() == 'china')
  1129.                     continue;
  1130.                 $invoice_ids $objInv->getXeroId();
  1131.                 $tenantId $objInv->getTenantId();
  1132.                 try {
  1133.                     $apiResponse $accountingApi->getInvoices($tenantIdnullnullnull$invoice_idsnullnullnullnullnullnullnull);
  1134.                     foreach ($apiResponse as $invoice) {
  1135.                         if ($invoice['status'] != $objInv->getXeroStatus()) {
  1136.                             $objInv->setXeroStatus($invoice['status']);
  1137.                             if (!isset($response['inv-updated'][$invoice['status']])) {
  1138.                                 $response['inv-updated'][$invoice['status']] = 1;
  1139.                             } else {
  1140.                                 $response['inv-updated'][$invoice['status']]++;
  1141.                             }
  1142.                         } else {
  1143.                             if (!isset($response['inv-current'][$invoice['status']])) {
  1144.                                 $response['inv-current'][$invoice['status']] = 1;
  1145.                             } else {
  1146.                                 $response['inv-current'][$invoice['status']]++;
  1147.                             }
  1148.                         }
  1149.                         $objInv->setUpdatedAt(new \DateTime());
  1150.                         $entityManager->flush();
  1151.                     }
  1152.                 } catch (\Exception $exception) {
  1153.                     $response['inv-status'] = 'ERROR';
  1154.                     $response['inv-message'] = 'API exception: ' $exception->getMessage();
  1155.                 }
  1156.             }
  1157.             $response['inv-status'] = 'OK';
  1158.         }
  1159.         if ($type == 'vinv' || $type == null) {
  1160.             $invoice_ids null;
  1161.             $vendorInvoiceRepository $entityManager->getRepository(VendorInvoice::class);
  1162.             $allObjVinv $vendorInvoiceRepository->findAllUpdatedBefore($datetime$limit);
  1163.             foreach ($allObjVinv as $objInv) {
  1164.                 if ($objInv->getXeroId() == null || $objInv->getXeroId() == 'china')
  1165.                     continue;
  1166.                 $invoice_ids $objInv->getXeroId();
  1167.                 $tenantId $objInv->getTenantId();
  1168.                 try {
  1169.                     $apiResponse $accountingApi->getInvoices($tenantIdnullnullnull$invoice_idsnullnullnullnullnullnullnull);
  1170.                     foreach ($apiResponse as $invoice) {
  1171.                         if ($invoice['status'] != $objInv->getXeroStatus()) {
  1172.                             $objInv->setXeroStatus($invoice['status']);
  1173.                             if (!isset($response['vinv-updated'][$invoice['status']])) {
  1174.                                 $response['vinv-updated'][$invoice['status']] = 1;
  1175.                             } else {
  1176.                                 $response['vinv-updated'][$invoice['status']]++;
  1177.                             }
  1178.                         } else {
  1179.                             if (!isset($response['vinv-current'][$invoice['status']])) {
  1180.                                 $response['vinv-current'][$invoice['status']] = 1;
  1181.                             } else {
  1182.                                 $response['vinv-current'][$invoice['status']]++;
  1183.                             }
  1184.                         }
  1185.                         $objInv->setUpdatedAt(new \DateTime());
  1186.                         $entityManager->flush();
  1187.                     }
  1188.                 } catch (\Exception $exception) {
  1189.                     $response['vinv-status'] = 'ERROR';
  1190.                     $response['vinv-message'] = 'API exception: ' $exception->getMessage();
  1191.                 }
  1192.             }
  1193.             $response['vinv-status'] = 'OK';
  1194.         }
  1195.         return $response;
  1196.     }
  1197.     // defaults to SG tenant ID (MT SG)
  1198.     public function fetchSalesOrders($soNumbers null$tenantId "771ca798-d2cf-4735-ab1b-1449300eda82"$dateFrom null)
  1199.     {
  1200.         set_time_limit(90);
  1201.         if ($this->getHasExpired()) {
  1202.             $this->renewToken($this->params);
  1203.         }
  1204.         $entityManager $this->em;
  1205.         $config \XeroAPI\XeroPHP\Configuration::getDefaultConfiguration()->setAccessToken((string) $this->getSession()->getToken());
  1206.         $accountingApi = new \XeroAPI\XeroPHP\Api\AccountingApi(
  1207.             new \GuzzleHttp\Client(),
  1208.             $config
  1209.         );
  1210.         $order null// string
  1211.         $contact_ids null// string[] | Filter by a comma-separated list of ContactIDs.
  1212.         $statuses null//array("SUBMITTED", "PAID", "AUTHORISED", "VOIDED", "DRAFT", "DELETED");
  1213.         $page 1// int | e.g. page=1 â€“ Up to 100 invoices will be returned in a single API call with line items
  1214.         // $dateFrom = new \DateTime($dateFrom) ?? new \DateTime("2023-01-01");
  1215.         $dateFrom = new \DateTime($dateFrom) ?? new \DateTime('-24 hours');
  1216.         if ($soNumbers) {
  1217.             $dateFrom = new \DateTime("2020-01-01");
  1218.         }
  1219.         $allApiResponses = [];
  1220.         $stopQuery false;
  1221.         $response['so-add'] = 0;
  1222.         $response['so-update'] = 0;
  1223.         $response['so-not-found'] = 0;
  1224.         $response['so-lineitems'] = 0;
  1225.         $response['so-removed-lineitems'] = 0;
  1226.         try {
  1227.             while (!$stopQuery) {
  1228.                 $apiResponse $accountingApi->getQuotes($tenantIdnull$dateFromnullnullnull$contact_ids$statuses$page$order$soNumbers);
  1229.                 if (sizeof($apiResponse) < 100) {
  1230.                     $stopQuery true;
  1231.                 }
  1232.                 if (sizeof($apiResponse) == 0) {
  1233.                     $response['so-not-found']++;
  1234.                     if($soNumbers$stopQuery true;
  1235.                     continue;
  1236.                 };
  1237.                 $page++;
  1238.                 $salesOrderRepository $this->em->getRepository(SalesOrder::class);
  1239.                 $currencyRepository $this->em->getRepository(Currency::class);
  1240.                 $xeroLineItemRepository $entityManager->getRepository(XeroLineItem::class);
  1241.                 $departmentRepository $entityManager->getRepository(Department::class);
  1242.                 foreach ($apiResponse as $so) {
  1243.                     $objSo $salesOrderRepository->findOneBy(['salesOrderId' => $so['quote_id']]);
  1244.                     if (!$objSo) {
  1245.                         // We create a new invoice
  1246.                         $objSo = new SalesOrder();
  1247.                         $objSo->setCreatedAt(new \DateTime());
  1248.                         $response['so-add']++;
  1249.                     } else {
  1250.                         $objSo->setUpdatedAt(new \DateTime());
  1251.                         $response['so-update']++;
  1252.                     }
  1253.                     if (isset($so['contact'])) {
  1254.                         $objSo->setXeroContactId($so['contact']['contact_id']);
  1255.                     }
  1256.                     $objSo->setSalesOrderNo($so['quote_number']);
  1257.                     $currency_code $so['currency_code'];
  1258.                     //print_r($currency_code);
  1259.                     $currency $currencyRepository->findOneBy(['iso' => $currency_code]);
  1260.                     //print_r($currency);
  1261.                     if (!$currency) {
  1262.                         $currency = new Currency();
  1263.                         $currency->setIso($currency_code);
  1264.                         $currency->setName($currency_code);
  1265.                         $currency->setCreatedAt(new \DateTime());
  1266.                         $currency->setSymbol('-');
  1267.                         $entityManager->persist($currency);
  1268.                         $entityManager->flush();
  1269.                         $entityManager->refresh($currency);
  1270.                     }
  1271.                     $objSo->setCurrency($currency);
  1272.                     $text_date $so['date'];
  1273.                     preg_match('#^/Date\((\d{10})#'$text_date$matches);
  1274.                     $dt = new \DateTime('@' $matches[1]);
  1275.                     $objSo->setQuoteDate($dt);
  1276.                     $objSo->setTotal($so['total']);
  1277.                     $objSo->setTotalUsd($this->currencyService->convertAtDate(date_format($dt"Y-m-d"), $currency_code"USD"$so['total']));
  1278.                     $objSo->setSubTotalUsd($this->currencyService->convertAtDate(date_format($dt"Y-m-d"), $currency_code"USD"$so['sub_total']));
  1279.                     $objSo->setSubtotal($so['sub_total']);
  1280.                     if (is_null($so['reference'])) {
  1281.                         $objSo->setReference('');
  1282.                     } else {
  1283.                         $objSo->setReference($so['reference']);
  1284.                     }
  1285.                     $objSo->setXeroId($so['quote_id']);
  1286.                     $objSo->setSalesOrderId($so['quote_id']);
  1287.                     $objSo->setXeroStatus($so['status']);
  1288.                     $objSo->setTenantId($tenantId);
  1289.                     $entityManager->persist($objSo);
  1290.                     $entityManager->flush();
  1291.                     if ($objSo->getCreator() == null) { // INV Creator 
  1292.                         $creator $this->fetchInvoiceHistory($objSo->getXeroId());
  1293.                         $objSo->setCreator($creator);
  1294.                         $entityManager->flush();
  1295.                         $entityManager->refresh($objSo);
  1296.                     }
  1297.                     if ($so['line_items']) { // INV Line Items
  1298.                         $availableLineItems = [];
  1299.                         // $response['so-lineitems'] = 0;
  1300.                         // $response['so-removed-lineitems'] = 0;
  1301.                         foreach ($so['line_items'] as $lineItem) {
  1302.                             // if($lineItem['item_code'] == null || !array_key_exists($lineItem['item_code'], $this->arr_item_codes)) continue; 
  1303.                             // if(!array_key_exists($lineItem['item_code'], $arr_item_codes)) continue;   
  1304.                             if ($lineItem['item_code'] != null && !array_key_exists(strtolower($lineItem['item_code']), $this->arr_item_codes))
  1305.                                 continue;
  1306.                             if ($lineItem['item_code'] == null && !preg_match($this->projectIdPattern$lineItem['description']) && ($lineItem['line_amount'] == null || $lineItem['line_amount'] == 0))
  1307.                                 continue;
  1308.                             $lineItemId $lineItem['line_item_id'];
  1309.                             $lineItemTotal $lineItem['line_amount'] + $lineItem['tax_amount'];
  1310.                             $lineItemSubTotal $lineItem['line_amount'];
  1311.                             $availableLineItems[] = $lineItemId;
  1312.                             $objLineItem $xeroLineItemRepository->findOneBy(['xeroId' => $lineItemId]) ?? new XeroLineItem();
  1313.                             if ($objLineItem->getId() == null) {
  1314.                                 $objLineItem->setCreatedAt(new \DateTimeImmutable());
  1315.                             } else {
  1316.                                 $objLineItem->setUpdatedAt(new \DateTimeImmutable());
  1317.                             }
  1318.                             if ($lineItem['item_code'] == null) {
  1319.                                 $lineItemDepartment null// default blank department
  1320.                                 $itemCode null// default blank item code
  1321.                                 $isIncluded false;
  1322.                             } else {
  1323.                                 $lineItemDepartment $departmentRepository->findOneBy(['id' => $this->arr_item_codes[strtolower($lineItem['item_code'])]]) ?? null;
  1324.                                 $isIncluded = !in_array(strtolower($lineItem['item_code']), $this->arr_excluded_item_codes);
  1325.                                 $itemCode $lineItem['item_code'];
  1326.                             };
  1327.                             $objLineItem->setParentNo($objSo->getSalesOrderNo());
  1328.                             $objLineItem->setSalesOrder($objSo);
  1329.                             $objLineItem->setXeroId($lineItemId);
  1330.                             $objLineItem->setTenantId($tenantId);
  1331.                             $objLineItem->setIsIncluded($isIncluded);
  1332.                             $objLineItem->setDepartment($lineItemDepartment);
  1333.                             $objLineItem->setDescription($lineItem['description']);
  1334.                             $objLineItem->setQuantity($lineItem['quantity']);
  1335.                             $objLineItem->setUnitAmount($lineItem['unit_amount']);
  1336.                             $objLineItem->setItemCode($itemCode);
  1337.                             $objLineItem->setAccountCode($lineItem['account_code']);
  1338.                             $objLineItem->setAccountId($lineItem['account_id']);
  1339.                             $objLineItem->setTotal($lineItemTotal);
  1340.                             $objLineItem->setSubtotal($lineItemSubTotal);
  1341.                             $objLineItem->setTotalUsd($this->currencyService->convertAtDate(date_format($dt"Y-m-d"), $currency_code"USD"$lineItemTotal));
  1342.                             $objLineItem->setSubTotalUsd($this->currencyService->convertAtDate(date_format($dt"Y-m-d"), $currency_code"USD"$lineItemSubTotal));
  1343.                             $entityManager->persist($objLineItem);
  1344.                             $entityManager->flush();
  1345.                             $response['so-lineitems']++;
  1346.                         }
  1347.                         $entityManager->refresh($objSo);
  1348.                         foreach ($objSo->getXeroLineItems() as $lineItem) {
  1349.                             if (in_array($lineItem->getXeroId(), $availableLineItems))
  1350.                                 continue;
  1351.                             $entityManager->remove($lineItem);
  1352.                             $entityManager->flush();
  1353.                             $response['so-removed-lineitems']++;
  1354.                         }
  1355.                     }
  1356.                     // $this->projectService->projectXeroAllocationEmail($objSo);
  1357.                 }
  1358.             }
  1359.         } catch (\Exception $exception) {
  1360.             $response['status'] = 'ERROR';
  1361.             $response['message'] = 'API exception: ' $exception->getMessage();
  1362.             return $response;
  1363.         }
  1364.         $response['status'] = 'OK';
  1365.         return $response;
  1366.     }
  1367.     public function fetchContacts($contactIds null$tenantId "771ca798-d2cf-4735-ab1b-1449300eda82"$date null)
  1368.     {
  1369.         if ($this->getHasExpired()) {
  1370.             $this->renewToken($this->params);
  1371.         }
  1372.         $entityManager $this->em;
  1373.         $config \XeroAPI\XeroPHP\Configuration::getDefaultConfiguration()->setAccessToken((string) $this->getSession()->getToken());
  1374.         $accountingApi = new \XeroAPI\XeroPHP\Api\AccountingApi(
  1375.             new \GuzzleHttp\Client(),
  1376.             $config
  1377.         );
  1378.         $page 1// int | e.g. page=1 â€“ Up to 100 contacts will be returned in a single API call with line items
  1379.         $stopQuery false;
  1380.         $response = ["Added" => 0"Updated" => 0"Update date filled" => 0];
  1381.         try {
  1382.             while (!$stopQuery) {
  1383.                 $apiResponse $accountingApi->getContacts($tenantId$datenullnull$contactIds$pagenullfalsenull);
  1384.                 if (sizeof($apiResponse) < 25) {
  1385.                     $stopQuery true;
  1386.                 }
  1387.                 $xeroContactRepository $this->em->getRepository(XeroContact::class);
  1388.                 foreach ($apiResponse as $xc) {
  1389.                     $objContact $xeroContactRepository->findOneBy(['xeroContactId' => $xc['contact_id']]);
  1390.                     preg_match('#^/Date\((\d{10})#'$xc->getUpdatedDateUtc(), $matches);
  1391.                     if (array_key_exists(1$matches)) {
  1392.                         $updatedDateUtc = new \DateTime('@' $matches[1]);
  1393.                     } else {
  1394.                         $updatedDateUtc = new \DateTime();
  1395.                     }
  1396.                     if (!$objContact) {
  1397.                         // We create a new contact
  1398.                         $objContact = new XeroContact();
  1399.                         $objContact->setXeroContactId($xc['contact_id']);
  1400.                         $objContact->setCreatedAt(new \DateTime());
  1401.                         $response["Added"]++;
  1402.                     } elseif ($objContact->getUpdatedAt()) {
  1403.                         // $objContact->setUpdatedAt(new \DateTime());
  1404.                         // $response["Updated"]++;
  1405.                         if ($objContact->getUpdatedAt()->format('Y-m-d H:i:s') < $updatedDateUtc->format('Y-m-d H:i:s')) {
  1406.                             $response['Updated']++;
  1407.                         } else {
  1408.                             continue;
  1409.                         }
  1410.                     } else {
  1411.                         $response['Update date filled']++;
  1412.                     };
  1413.                     $objContact->setName($xc['name']);
  1414.                     $objContact->setTenantId($tenantId);
  1415.                     $objContact->setUpdatedAt($updatedDateUtc);
  1416.                     if ($objContact->getId() === null) {
  1417.                         $entityManager->persist($objContact);
  1418.                     }
  1419.                     $entityManager->flush();
  1420.                 }
  1421.                 $page++;
  1422.             } //end while
  1423.         } catch (\Exception $exception) {
  1424.             $response['status'] = 'ERROR';
  1425.             $response['message'] = 'API exception: ' $exception->getMessage();
  1426.             return $response;
  1427.         }
  1428.         $response['status'] = 'OK';
  1429.         return $response;
  1430.     }
  1431.     public function processWebhookObjects()
  1432.     {
  1433.         $webhook_objects $this->em->getRepository(XeroWebhook::class)->findBy(['completed' => 0]);
  1434.         $response['status'] = 'OK';
  1435.         $response['total'] = 0;
  1436.         foreach ($webhook_objects as $who) {
  1437.             try {
  1438.                 $obj json_decode($who->getObject());
  1439.                 foreach ($obj->events as $event) {
  1440.                     if ($event->eventCategory === "INVOICE") {
  1441.                         $xeroContactRepository $this->em->getRepository(XeroContact::class);
  1442.                         $resp $this->fetchInvoices(null$event->resourceId$event->tenantIdnull, ['VOIDED'], true);
  1443.                         if ($resp) {
  1444.                             $who->setCompleted(1);
  1445.                             $this->em->persist($who);
  1446.                             $this->em->flush();
  1447.                             $response['total']++;
  1448.                         } else {
  1449.                             $response['status'] = 'ERROR';
  1450.                             // $response['message'] = $resp['message'];
  1451.                         }
  1452.                         if (!is_object($resp)) continue;
  1453.                         $contact $xeroContactRepository->findOneBy(['xeroContactId' => $resp->getXeroContactId()]);
  1454.                         if (!$contact) {
  1455.                             // fetch contact first if not yet in db
  1456.                             $this->fetchContacts($resp->getXeroContactId(), $event->tenantId);
  1457.                             $contact $xeroContactRepository->findOneBy(['xeroContactId' => $resp->getXeroContactId()]);
  1458.                         }
  1459.                         $this->mattermostService->sendPostRequestToMattermost($resp$event$contact);
  1460.                     } else if ($event->eventCategory === "CONTACT") {
  1461.                         $resp $this->fetchContacts($event->resourceId$event->tenantId);
  1462.                         if ($resp['status'] == 'OK') {
  1463.                             $who->setCompleted(1);
  1464.                             $this->em->persist($who);
  1465.                             $this->em->flush();
  1466.                             $response['total']++;
  1467.                         } else {
  1468.                             $response['status'] = 'ERROR';
  1469.                             $response['message'] = $resp['message'];
  1470.                         }
  1471.                     }
  1472.                 }
  1473.             } catch (\Exception GuzzleException $exception) {
  1474.                 $response['status'] = 'ERROR';
  1475.                 return $response;
  1476.             }
  1477.         }
  1478.         return $response;
  1479.     }
  1480.     public function checkCredential($test)
  1481.     {
  1482.         $isValid $this->getHasExpired() ? $this->renewToken($this->params) : true;
  1483.         if (!$test && !$isValid) {
  1484.             $provider = new \League\OAuth2\Client\Provider\GenericProvider([
  1485.                 'clientId' => $this->params->get('xeroClientId'),
  1486.                 'clientSecret' => $this->params->get('xeroClientSecret'),
  1487.                 'redirectUri' => $this->params->get('xeroRedirectURI'),
  1488.                 'urlAuthorize' => 'https://login.xero.com/identity/connect/authorize',
  1489.                 'urlAccessToken' => 'https://identity.xero.com/connect/token',
  1490.                 'urlResourceOwnerDetails' => 'https://api.xero.com/api.xro/2.0/Organisation'
  1491.             ]);
  1492.             // Scope defines the data your app has permission to access.
  1493.             // Learn more about scopes at https://developer.xero.com/documentation/oauth2/scopes
  1494.             $options = [
  1495.                 'scope' => ['openid email profile offline_access accounting.settings.read accounting.transactions.read accounting.contacts.read accounting.journals.read accounting.reports.read accounting.attachments.read']
  1496.             ];
  1497.             // This returns the authorizeUrl with necessary parameters applied (e.g. state).
  1498.             // $authorizationUrl = $provider->getAuthorizationUrl($options);
  1499.             // generate url from route: xero_authorize
  1500.             $authorizationUrl $this->urlGenerator->generate('xero_authorize', [], UrlGeneratorInterface::ABSOLUTE_URL);
  1501.             $message "Please login using Xero credential here: " $authorizationUrl;
  1502.             $to $_SERVER['APP_ENV'] == "prod" ? ['devops@mediatropy.com'] : [$this->params->get('systemEmailOps')];
  1503.             $this->mailgunService->sendEmailAdvanced(
  1504.                 [
  1505.                     'subject' => "WARNING: Xero Credential Expired, Login Required",
  1506.                     'to' => $to// TODO: Might need to put it as parameter in the future (for devops)
  1507.                     'template' => 'email/email-internal/default.html.twig',
  1508.                     'params' => [
  1509.                         'message' => $message,
  1510.                     ]
  1511.                 ],
  1512.                 false
  1513.             );
  1514.         }
  1515.         return $isValid;
  1516.     }
  1517.     public function fetchXeroUsers($id)
  1518.     {
  1519.         if ($this->getHasExpired()) {
  1520.             $this->renewToken($this->params);
  1521.         }
  1522.         $config \XeroAPI\XeroPHP\Configuration::getDefaultConfiguration()->setAccessToken((string) $this->getSession()->getToken());
  1523.         $apiInstance = new \XeroAPI\XeroPHP\Api\AccountingApi(
  1524.             new \GuzzleHttp\Client(),
  1525.             $config
  1526.         );
  1527.         $tenantId = (strlen($id) <= 2) ? $this->getTenantId($id) : $id;
  1528.         $ifModifiedSince = new \DateTime("2014-02-06T12:17:43.202-08:00");
  1529.         $where ""//IsSubscriber==true
  1530.         $order "LastName ASC";
  1531.         try {
  1532.             $results $apiInstance->getUsers($tenantId$ifModifiedSince$where$order);
  1533.             // var_dump($result);
  1534.             // foreach ($results as $result) {
  1535.             // var_dump($result->getUpdatedDateUtc());
  1536.             // };
  1537.             // die;
  1538.             $response = ['Added' => 0'Updated' => 0'Attached to User' => 0'Detached from User' => 0'message' => ''];
  1539.             $xeroUserRepository $this->em->getRepository(XeroUser::class);
  1540.             foreach ($results as $result) {
  1541.                 $xeroUser $xeroUserRepository->findOneBy(['xeroUserId' => $result->getUserID()]);
  1542.                 preg_match('#^/Date\((\d{10})#'$result->getUpdatedDateUtc(), $matches);
  1543.                 if (array_key_exists(1$matches)) {
  1544.                     $updatedDateUtc = new \DateTimeImmutable('@' $matches[1]);
  1545.                 } else {
  1546.                     $updatedDateUtc = new \DateTimeImmutable();
  1547.                 }
  1548.                 if (!$xeroUser) {
  1549.                     $xeroUser = new XeroUser();
  1550.                     $xeroUser->setXeroUserId($result->getUserID());
  1551.                     $xeroUser->setCreatedAt(new \DateTimeImmutable());
  1552.                     $response['Added']++;
  1553.                 } else {
  1554.                     if ($xeroUser->getUpdatedAt() != null && $xeroUser->getUpdatedAt()->format('Y-m-d H:i:s') < $updatedDateUtc->format('Y-m-d H:i:s')) {
  1555.                         $response['Updated']++;
  1556.                     } else {
  1557.                         continue;
  1558.                     }
  1559.                 }
  1560.                 $xeroUser->setFirstName($result->getFirstName());
  1561.                 $xeroUser->setLastName($result->getLastName());
  1562.                 $xeroUser->setEmail($result->getEmailAddress());
  1563.                 $xeroUser->setRole($result->getOrganisationRole());
  1564.                 $xeroUser->setUpdatedAt($updatedDateUtc);
  1565.                 $xeroUser->setTenantId($tenantId);
  1566.                 if ($xeroUser->getId() === null) {
  1567.                     $this->em->persist($xeroUser);
  1568.                 } else {
  1569.                     if ($xeroUser->getUser() && $xeroUser->getUser()->getEmail() !== $result->getEmailAddress()) {
  1570.                         $xeroUser->getUser()->removeXeroUser($xeroUser);
  1571.                         $response['Detached from User']++;
  1572.                     }
  1573.                 }
  1574.                 $user $this->em->getRepository(User::class)->findOneBy(['email' => $result->getEmailAddress()]);
  1575.                 if ($user) {
  1576.                     $user->addXeroUser($xeroUser);
  1577.                     $response['Attached to User']++;
  1578.                 }
  1579.                 $this->em->flush();
  1580.             };
  1581.             $response['status'] = 'OK';
  1582.         } catch (Exception $e) {
  1583.             $response['status'] = 'ERROR';
  1584.             $response['message'] = 'Exception when calling AccountingApi->getUsers: ' $e->getMessage() . PHP_EOL;
  1585.         }
  1586.         return $response;
  1587.     }
  1588.     public function getXeroUser($id$email)
  1589.     {
  1590.         if ($this->getHasExpired()) {
  1591.             $this->renewToken($this->params);
  1592.         }
  1593.         $config \XeroAPI\XeroPHP\Configuration::getDefaultConfiguration()->setAccessToken((string) $this->getSession()->getToken());
  1594.         $apiInstance = new \XeroAPI\XeroPHP\Api\AccountingApi(
  1595.             new \GuzzleHttp\Client(),
  1596.             $config
  1597.         );
  1598.         $tenantId = (strlen($id) <= 2) ? $this->getTenantId($id) : $id;
  1599.         try {
  1600.             $apiResponses $apiInstance->getUsers($tenantIdnull'EmailAddress=="' $email '"');
  1601.             if (count($apiResponses) > 0) {
  1602.                 foreach ($apiResponses as $apiResponse) {
  1603.                     $xeroUser $this->em->getRepository(XeroUser::class)->findOneBy(['xeroUserId' => $apiResponse->getUserID()]);
  1604.                     if (!$xeroUser) {
  1605.                         preg_match('#^/Date\((\d{10})#'$apiResponse->getUpdatedDateUtc(), $matches);
  1606.                         if (array_key_exists(1$matches)) {
  1607.                             $updatedDateUtc = new \DateTimeImmutable('@' $matches[1]);
  1608.                         } else {
  1609.                             $updatedDateUtc = new \DateTimeImmutable();
  1610.                         }
  1611.                         $xeroUser = new XeroUser();
  1612.                         $xeroUser->setXeroUserId($apiResponse->getUserID());
  1613.                         $xeroUser->setCreatedAt(new \DateTimeImmutable());
  1614.                     };
  1615.                     $xeroUser->setFirstName($apiResponse->getFirstName());
  1616.                     $xeroUser->setLastName($apiResponse->getLastName());
  1617.                     $xeroUser->setEmail($apiResponse->getEmailAddress());
  1618.                     $xeroUser->setRole($apiResponse->getOrganisationRole());
  1619.                     $xeroUser->setUpdatedAt($updatedDateUtc);
  1620.                     $xeroUser->setTenantId($tenantId);
  1621.                     if ($xeroUser->getId() === null) {
  1622.                         $this->em->persist($xeroUser);
  1623.                     };
  1624.                     $user $this->em->getRepository(User::class)->findOneBy(['email' => $apiResponse->getEmailAddress()]);
  1625.                     if ($user) {
  1626.                         $user->addXeroUser($xeroUser);
  1627.                     }
  1628.                     $this->em->flush();
  1629.                 }
  1630.                 $response['status'] = 'OK';
  1631.                 $response['message'] = 'Xero user found and linked.';
  1632.             } else {
  1633.                 $response['status'] = 'ERROR';
  1634.                 $response['message'] = 'Xero user not found.';
  1635.             }
  1636.         } catch (Exception $e) {
  1637.             $response['status'] = 'ERROR';
  1638.             $response['message'] = 'Exception when calling AccountingApi->getUser: ' $e->getMessage() . PHP_EOL;
  1639.         }
  1640.         return $response;
  1641.     }
  1642.     public function createXeroContact($id$vendor$name)
  1643.     {
  1644.         if ($this->getHasExpired()) {
  1645.             $this->renewToken($this->params);
  1646.         }
  1647.         $config \XeroAPI\XeroPHP\Configuration::getDefaultConfiguration()->setAccessToken((string) $this->getSession()->getToken());
  1648.         $apiInstance = new \XeroAPI\XeroPHP\Api\AccountingApi(
  1649.             new \GuzzleHttp\Client(),
  1650.             $config
  1651.         );
  1652.         $tenantId = (strlen($id) <= 2) ? $this->getTenantId($id) : $id;
  1653.         $summarizeErrors true;
  1654.         $contact = new \XeroAPI\XeroPHP\Models\Accounting\Contact;
  1655.         $contact->setName($name);
  1656.         $contacts = new \XeroAPI\XeroPHP\Models\Accounting\Contacts;
  1657.         $arr_contacts = [];
  1658.         array_push($arr_contacts$contact);
  1659.         $contacts->setContacts($arr_contacts);
  1660.         try {
  1661.             $results $apiInstance->createContacts($tenantId$contacts$summarizeErrors);
  1662.             foreach ($results->getContacts() as $result) {
  1663.                 try {
  1664.                     preg_match('#^/Date\((\d{10})#'$result->getUpdatedDateUtc(), $matches);
  1665.                     if (array_key_exists(1$matches)) {
  1666.                         $updatedDateUtc = new \DateTime('@' $matches[1]);
  1667.                     } else {
  1668.                         $updatedDateUtc = new \DateTime();
  1669.                     }
  1670.                     $objContact = new XeroContact();
  1671.                     $objContact->setXeroContactId($result->getContactId());
  1672.                     $objContact->setCreatedAt(new \DateTime());
  1673.                     $objContact->setName($result->getName());
  1674.                     $objContact->setTenantId($tenantId);
  1675.                     $objContact->setUpdatedAt($updatedDateUtc);
  1676.                     $objContact->setVendor($vendor);
  1677.                     $this->em->persist($objContact);
  1678.                     $this->em->flush();
  1679.                 } catch (Exception $e) {
  1680.                     $response['message'] = 'Exception when creating contact on HRP: ' $e->getMessage() . PHP_EOL;
  1681.                 };
  1682.                 $response['status'] = 'OK';
  1683.                 $response['result'] = $result;
  1684.             }
  1685.         } catch (Exception $e) {
  1686.             $response['status'] = 'ERROR';
  1687.             $response['message'] = 'Exception when calling AccountingApi->createContacts: ' $e->getMessage() . PHP_EOL;
  1688.         }
  1689.         return $response;
  1690.     }
  1691.     public function createReceipt($id$contactId$userId$receiptDetails)
  1692.     {
  1693.         if ($this->getHasExpired()) {
  1694.             $this->renewToken($this->params);
  1695.         }
  1696.         $config \XeroAPI\XeroPHP\Configuration::getDefaultConfiguration()->setAccessToken((string) $this->getSession()->getToken());
  1697.         $apiInstance = new \XeroAPI\XeroPHP\Api\AccountingApi(
  1698.             new \GuzzleHttp\Client(),
  1699.             $config
  1700.         );
  1701.         $tenantId = (strlen($id) <= 2) ? $this->getTenantId($id) : $id;
  1702.         $unitdp 2;
  1703.         $contact = new \XeroAPI\XeroPHP\Models\Accounting\Contact;
  1704.         $contact->setContactID($contactId);
  1705.         $user = new \XeroAPI\XeroPHP\Models\Accounting\User;
  1706.         $user->setUserID($userId);
  1707.         $lineItem = new \XeroAPI\XeroPHP\Models\Accounting\LineItem;
  1708.         // $lineItem->setDescription('Test from HRP xero integration - ignore');
  1709.         // $lineItem->setUnitAmount(1.0);
  1710.         // $lineItem->setAccountCode('438');
  1711.         // $lineItem->setTaxType('NONE');
  1712.         // $lineItem->setTaxType('INPUT');
  1713.         $lineItem->setQuantity(1.0);
  1714.         $lineItem->setDescription(($_SERVER['APP_ENV'] !== 'prod' "[TEST] " "") . $receiptDetails['description']);
  1715.         $lineItem->setUnitAmount($receiptDetails['unit_amount']);
  1716.         $lineItem->setAccountCode($receiptDetails['account_code']);
  1717.         $lineItem->setTaxType($receiptDetails['tax_type']);
  1718.         if ($receiptDetails['tracking'] != null) {
  1719.             $lineItemTracking = new \XeroAPI\XeroPHP\Models\Accounting\LineItemTracking;
  1720.             $lineItemTracking->setName($receiptDetails['tracking']['name']);
  1721.             $lineItemTracking->setOption($receiptDetails['tracking']['option']);
  1722.             $lineItem->setTracking([$lineItemTracking]);
  1723.         };
  1724.         $receipt = new \XeroAPI\XeroPHP\Models\Accounting\Receipt;
  1725.         $receipt->setContact($contact);
  1726.         $receipt->setUser($user);
  1727.         $receipt->setLineItems([$lineItem]);
  1728.         $receipt->setLineAmountTypes(\XeroAPI\XeroPHP\Models\Accounting\LineAmountTypes::INCLUSIVE);
  1729.         $receipt->setStatus(\XeroAPI\XeroPHP\Models\Accounting\Receipt::STATUS_DRAFT);
  1730.         $receipt->setDate($receiptDetails['date'] instanceof \DateTimeImmutable $receiptDetails['date']->format('Y-m-d') : $receiptDetails['date']);
  1731.         $receipt->setReference($receiptDetails['reference']);
  1732.         $receipts = new \XeroAPI\XeroPHP\Models\Accounting\Receipts;
  1733.         $receipts->setReceipts([$receipt]);
  1734.         try {
  1735.             $result $apiInstance->createReceipt($tenantId$receipts$unitdp);
  1736.             foreach ($result->getReceipts() as $result) {
  1737.                 $response['status'] = 'OK';
  1738.                 $response['result'] = $result;
  1739.             }
  1740.         } catch (Exception $e) {
  1741.             $response['status'] = 'ERROR';
  1742.             $response['message'] = 'Exception when calling AccountingApi->createReceipt: ' $e->getMessage() . PHP_EOL;
  1743.         }
  1744.         return $response;
  1745.     }
  1746.     public function getReceipts($id$date)
  1747.     {
  1748.         if ($this->getHasExpired()) {
  1749.             $this->renewToken($this->params);
  1750.         }
  1751.         $config \XeroAPI\XeroPHP\Configuration::getDefaultConfiguration()->setAccessToken((string) $this->getSession()->getToken());
  1752.         $apiInstance = new \XeroAPI\XeroPHP\Api\AccountingApi(
  1753.             new \GuzzleHttp\Client(),
  1754.             $config
  1755.         );
  1756.         $tenantId = (strlen($id) <= 2) ? $this->getTenantId($id) : $id;
  1757.         try {
  1758.             $result $apiInstance->getReceipts($tenantId$date);
  1759.             $response['status'] = 'OK';
  1760.             $response['result'] = $result;
  1761.         } catch (Exception $e) {
  1762.             $response['status'] = 'ERROR';
  1763.             $response['message'] = 'Exception when calling AccountingApi->getReceipts: ' $e->getMessage() . PHP_EOL;
  1764.         }
  1765.         return $response;
  1766.     }
  1767.     public function createReceiptAttachment($id$receiptId$fileName$filePath)
  1768.     {
  1769.         if ($this->getHasExpired()) {
  1770.             $this->renewToken($this->params);
  1771.         }
  1772.         // 1ff96b79-b653-43ec-8514-b2890beb9225
  1773.         $config \XeroAPI\XeroPHP\Configuration::getDefaultConfiguration()->setAccessToken((string) $this->getSession()->getToken());
  1774.         $apiInstance = new \XeroAPI\XeroPHP\Api\AccountingApi(
  1775.             new \GuzzleHttp\Client(),
  1776.             $config
  1777.         );
  1778.         $tenantId = (strlen($id) <= 2) ? $this->getTenantId($id) : $id;
  1779.         $body file_get_contents($filePath);
  1780.         try {
  1781.             $result $apiInstance->createReceiptAttachmentByFileName($tenantId$receiptId$fileName$body);
  1782.             foreach ($result->getAttachments() as $result) {
  1783.                 $response['status'] = 'OK';
  1784.                 $response['result'] = $result;
  1785.             }
  1786.         } catch (Exception $e) {
  1787.             $response['status'] = 'ERROR';
  1788.             $response['message'] = 'Exception when calling AccountingApi->createReceiptAttachmentByFileName: ' $e->getMessage() . PHP_EOL;
  1789.         }
  1790.         return $response;
  1791.     }
  1792.     public function createExpenseClaims($id$userId$receiptId)
  1793.     {
  1794.         if ($this->getHasExpired()) {
  1795.             $this->renewToken($this->params);
  1796.         }
  1797.         $config \XeroAPI\XeroPHP\Configuration::getDefaultConfiguration()->setAccessToken((string) $this->getSession()->getToken());
  1798.         $apiInstance = new \XeroAPI\XeroPHP\Api\AccountingApi(
  1799.             new \GuzzleHttp\Client(),
  1800.             $config
  1801.         );
  1802.         $tenantId = (strlen($id) <= 2) ? $this->getTenantId($id) : $id;
  1803.         $currDate = new \DateTime();
  1804.         $user = new \XeroAPI\XeroPHP\Models\Accounting\User;
  1805.         $user->setUserID($userId);
  1806.         $receipt = new \XeroAPI\XeroPHP\Models\Accounting\Receipt;
  1807.         $receipt->setReceiptID($receiptId);
  1808.         $receipt->setDate($currDate);
  1809.         $receipts = [];
  1810.         array_push($receipts$receipt);
  1811.         $expenseClaim = new \XeroAPI\XeroPHP\Models\Accounting\ExpenseClaim;
  1812.         $expenseClaim->setStatus(\XeroAPI\XeroPHP\Models\Accounting\ExpenseClaim::STATUS_SUBMITTED);
  1813.         $expenseClaim->setUser($user);
  1814.         $expenseClaim->setReceipts($receipts);
  1815.         $expenseClaims = new \XeroAPI\XeroPHP\Models\Accounting\ExpenseClaims;
  1816.         $arr_expense_claims = [];
  1817.         array_push($arr_expense_claims$expenseClaim);
  1818.         $expenseClaims->setExpenseClaims($arr_expense_claims);
  1819.         try {
  1820.             $result $apiInstance->createExpenseClaims($tenantId$expenseClaims);
  1821.             foreach ($result->getExpenseClaims() as $result) {
  1822.                 $response['status'] = 'OK';
  1823.                 $response['result'] = $result;
  1824.             }
  1825.         } catch (Exception $e) {
  1826.             $response['status'] = 'ERROR';
  1827.             $response['message'] = 'Exception when calling AccountingApi->createExpenseClaims: ' $e->getMessage() . PHP_EOL;
  1828.         }
  1829.         return $response;
  1830.     }
  1831.     public function fetchExpenseClaims($id$date null)
  1832.     {
  1833.         if ($this->getHasExpired()) {
  1834.             $this->renewToken($this->params);
  1835.         }
  1836.         $config \XeroAPI\XeroPHP\Configuration::getDefaultConfiguration()->setAccessToken((string) $this->getSession()->getToken());
  1837.         $apiInstance = new \XeroAPI\XeroPHP\Api\AccountingApi(
  1838.             new \GuzzleHttp\Client(),
  1839.             $config
  1840.         );
  1841.         $tenantId = (strlen($id) <= 2) ? $this->getTenantId($id) : $id;
  1842.         try {
  1843.             $result $apiInstance->getExpenseClaims($tenantId$date);
  1844.             $response['status'] = 'OK';
  1845.             $response['result'] = $result;
  1846.         } catch (Exception $e) {
  1847.             $response['status'] = 'ERROR';
  1848.             $response['message'] = 'Exception when calling AccountingApi->getExpenseClaims: ' $e->getMessage() . PHP_EOL;
  1849.         }
  1850.         return $response;
  1851.     }
  1852.     public function updateXeroClaim($id$claimId)
  1853.     {
  1854.         if ($this->getHasExpired()) {
  1855.             $this->renewToken($this->params);
  1856.         }
  1857.         $config \XeroAPI\XeroPHP\Configuration::getDefaultConfiguration()->setAccessToken((string) $this->getSession()->getToken());
  1858.         $apiInstance = new \XeroAPI\XeroPHP\Api\AccountingApi(
  1859.             new \GuzzleHttp\Client(),
  1860.             $config
  1861.         );
  1862.         $tenantId = (strlen($id) <= 2) ? $this->getTenantId($id) : $id;
  1863.         try {
  1864.             $expenseClaim $apiInstance->getExpenseClaim($tenantId$claimId);
  1865.             // Need to update editable fields
  1866.             $result $apiInstance->updateExpenseClaim($tenantId$claimId$expenseClaim);
  1867.             return $result;
  1868.         } catch (Exception $e) {
  1869.             return 'Exception when calling AccountingApi->updateExpenseClaim: ' $e->getMessage() . PHP_EOL;
  1870.         }
  1871.     }
  1872.     public function getExpenseClaimStatus($id$claimId)
  1873.     {
  1874.         if ($this->getHasExpired()) {
  1875.             $this->renewToken($this->params);
  1876.         }
  1877.         $config \XeroAPI\XeroPHP\Configuration::getDefaultConfiguration()->setAccessToken((string) $this->getSession()->getToken());
  1878.         $apiInstance = new \XeroAPI\XeroPHP\Api\AccountingApi(
  1879.             new \GuzzleHttp\Client(),
  1880.             $config
  1881.         );
  1882.         $tenantId = (strlen($id) <= 2) ? $this->getTenantId($id) : $id;
  1883.         try {
  1884.             $apiResponse $apiInstance->getExpenseClaim($tenantId$claimId);
  1885.             return $apiResponse->getExpenseClaims()[0]->getStatus();
  1886.         } catch (Exception $e) {
  1887.             return null;
  1888.         }
  1889.     }
  1890.     public function deleteExpenseClaim($id$expenseClaimId)
  1891.     {
  1892.         if ($this->getHasExpired()) {
  1893.             $this->renewToken($this->params);
  1894.         }
  1895.         $config \XeroAPI\XeroPHP\Configuration::getDefaultConfiguration()->setAccessToken((string) $this->getSession()->getToken());
  1896.         $apiInstance = new \XeroAPI\XeroPHP\Api\AccountingApi(
  1897.             new \GuzzleHttp\Client(),
  1898.             $config
  1899.         );
  1900.         $tenantId = (strlen($id) <= 2) ? $this->getTenantId($id) : $id;
  1901.         try {
  1902.             $expenseClaim $apiInstance->getExpenseClaim($tenantId$expenseClaimId);
  1903.             $expenseClaim->setStatus('DELETED');
  1904.             $result $apiInstance->updateExpenseClaim($tenantId$expenseClaimId$expenseClaim);
  1905.             return ['status' => 'OK''result' => $result];
  1906.         } catch (\Exception $e) {
  1907.             echo 'Exception when calling AccountingApi->updateExpenseClaim: '$e->getMessage(), PHP_EOL;
  1908.             return null;
  1909.         }
  1910.     }
  1911.     public function getLatestBill($id$date$status null$filename null$inv null)
  1912.     {
  1913.         if (!in_array($id, ['SG''ID''HK'])) {
  1914.             if ($inv) {
  1915.                 $expenseInvoice $this->em->getRepository(ExpenseRequestInvoice::class)->find($inv);
  1916.                 if ($expenseInvoice) {
  1917.                     $expenseInvoice->setXeroActionId('not-applicable');
  1918.                     $expenseInvoice->setXeroStatus('DRAFT');
  1919.                     $this->em->flush();
  1920.                 }
  1921.             }
  1922.             $response['status'] = 'ERROR';
  1923.             $response['message'] = 'Linking to Xero not applicable';
  1924.             return $response;
  1925.         };
  1926.         if ($this->getHasExpired()) {
  1927.             $this->renewToken($this->params);
  1928.         }
  1929.         $config \XeroAPI\XeroPHP\Configuration::getDefaultConfiguration()->setAccessToken((string) $this->getSession()->getToken());
  1930.         $apiInstance = new \XeroAPI\XeroPHP\Api\AccountingApi(
  1931.             new \GuzzleHttp\Client(),
  1932.             $config
  1933.         );
  1934.         $tenantId = (strlen($id) <= 2) ? $this->getTenantId($id) : $id;
  1935.         $response = [];
  1936.         $invoices $apiInstance->getInvoices($tenantId$date'Type=="ACCPAY"''Date DESC'nullnullnull$status1);
  1937.         foreach ($invoices as $invoice) {
  1938.             $invoiceId $invoice->getInvoiceID();
  1939.             $history $apiInstance->getInvoiceHistory($tenantId$invoiceId);
  1940.             if ($invoice->getHasAttachments() == true && $history[0]->getDetails() == 'Received through Email-to-Bill') {
  1941.                 if ($this->em->getRepository(ExpenseRequestInvoice::class)->findOneBy(['xeroActionId' => $invoiceId])) {
  1942.                     continue;
  1943.                 }
  1944.                 if ($filename) {
  1945.                     $attachments $apiInstance->getInvoiceAttachments($tenantId$invoiceId);
  1946.                     foreach ($attachments as $attachment) {
  1947.                         if ($attachment->getFileName() == $filename) {
  1948.                             $response['status'] = 'OK';
  1949.                             $response['message'] = 'Bill found';
  1950.                             $response['result'] = $invoice;
  1951.                             if ($inv) {
  1952.                                 $expenseInvoice $this->em->getRepository(ExpenseRequestInvoice::class)->find($inv);
  1953.                                 if ($expenseInvoice) {
  1954.                                     $expenseInvoice->setXeroActionId($invoice->getInvoiceId());
  1955.                                     $expenseInvoice->setXeroStatus($invoice->getStatus());
  1956.                                     $this->em->flush();
  1957.                                     $response['message'] = 'Bill found and linked to Expense Request';
  1958.                                 } else {
  1959.                                     $response['message'] = 'Bill found but not linked to Expense Request';
  1960.                                 }
  1961.                             }
  1962.                         }
  1963.                     }
  1964.                 }
  1965.             }
  1966.         }
  1967.         $response['status'] = 'OK';
  1968.         if (!isset($response['result'])) {
  1969.             if ($filename) {
  1970.                 $response['message'] = 'Bill not found';
  1971.             } else {
  1972.                 $response['result'] = $invoices;
  1973.             }
  1974.         }
  1975.         return $response;
  1976.     }
  1977.     public function getBillStatus($id$claimId)
  1978.     {
  1979.         if ($this->getHasExpired()) {
  1980.             $this->renewToken($this->params);
  1981.         }
  1982.         $config \XeroAPI\XeroPHP\Configuration::getDefaultConfiguration()->setAccessToken((string) $this->getSession()->getToken());
  1983.         $accountingApi = new \XeroAPI\XeroPHP\Api\AccountingApi(
  1984.             new \GuzzleHttp\Client(),
  1985.             $config
  1986.         );
  1987.         $tenantId = (strlen($id) <= 2) ? $this->getTenantId($id) : $id;
  1988.         try {
  1989.             $apiResponse $accountingApi->getInvoice($tenantId$claimId);
  1990.             return $apiResponse->getInvoices()[0]->getStatus();
  1991.         } catch (\Exception $e) {
  1992.             return null;
  1993.         }
  1994.         return null;
  1995.     }
  1996.     public function refreshExpenseClaimsStatus($testMode false)
  1997.     {
  1998.         $response = [];
  1999.         $expenseClaims $this->em->getRepository(ExpenseRequest::class)->findAllElligibleForPayment();
  2000.         foreach ($expenseClaims as $expenseClaim) {
  2001.             if (count($expenseClaim->getExpenseRequestInvoices()) == 0) continue;
  2002.             $invoices = [];
  2003.             foreach ($expenseClaim->getExpenseRequestInvoices() as $expenseRequestInvoice) {
  2004.                 $update false;
  2005.                 if ($expenseRequestInvoice->getSentHistory() == null || count($expenseRequestInvoice->getSentHistory()) == 0) continue;
  2006.                 $latestAction end($expenseRequestInvoice->getSentHistory()['history']);
  2007.                 switch (strtoupper($latestAction['action'])) {
  2008.                     case 'BILL':
  2009.                         $apiResponse $this->getBillStatus($latestAction['to'], $expenseRequestInvoice->getXeroActionId());
  2010.                         break;
  2011.                     case 'CLAIM':
  2012.                         $apiResponse $this->getExpenseClaimStatus($latestAction['to'], $expenseRequestInvoice->getXeroActionId());
  2013.                         break;
  2014.                     default:
  2015.                         $apiResponse null;
  2016.                         break;
  2017.                 }
  2018.                 if ($expenseRequestInvoice->getXeroStatus() != $apiResponse) {
  2019.                     $status $expenseRequestInvoice->getXeroStatus() ? $expenseRequestInvoice->getXeroStatus() . ' > ' $apiResponse $apiResponse;
  2020.                     $expenseRequestInvoice->setXeroStatus($apiResponse);
  2021.                     $update true;
  2022.                     if (!$testMode$this->em->flush();
  2023.                 } else {
  2024.                     $status $apiResponse;
  2025.                 }
  2026.                 $invoices[] = [
  2027.                     'action' => $latestAction['action'],
  2028.                     'xeroId' => $expenseRequestInvoice->getXeroActionId(),
  2029.                     'status' => $status,
  2030.                     'update' => $update
  2031.                 ];
  2032.             }
  2033.             $response[] = [
  2034.                 'title' => $expenseClaim->getTitle(),
  2035.                 'invoices' => $invoices
  2036.             ];
  2037.         }
  2038.         return $response;
  2039.     }
  2040. }