XeroAPI/xero-php-oauth2

Example code to create an invoice

githubjonny opened this issue · 17 comments

It would be a massive help if the script came with an example on submitting an invoice to drafts in the authorizedResource.php (or in a separate file) e.g. create an invoice with 3 example lineitems.

As it stands, its easy to get the starter php etc up and running and add customers and customer data but very hard (impossible for me at the moment see https://stackoverflow.com/questions/65232512/xero-api-struggling-to-add-a-basic-invoice) to get invoices setup.

A simple example would be a massive help.

@githubjonny - Have you checked out our companion app for this SDK?
https://github.com/XeroAPI/xero-php-oauth2-app

It demonstrates many of the calls including createInvoice.
https://github.com/XeroAPI/xero-php-oauth2-app/blob/7a6e4240a24f192d25864ca8ef5ca1573a2a3094/example.php#L1442

Hi @SidneyAllen , thank you for your reply.

Yes, I have tried the code from example.php but i'm finding it very hard to combine the example given with the items I want to add to the invoice. Been trying to sus it out for 2 days but not really got anywhere.

In oauth1 there was an example xml insert which was perfect as it gave me the backbone to work with and in turn then expand on and get working. However, I am aware that oauth1 ends this month (?).

Basically, I just need to work out how set up very very basic submission of a draft invoice into xero using the php oauth (e.g. just 3 products).

e.g.

<?php
  ini_set('display_errors', 'On');
  require __DIR__ . '/vendor/autoload.php';
  require_once('storage.php');

  // Use this class to deserialize error caught
  use XeroAPI\XeroPHP\AccountingObjectSerializer;

  // Storage Classe uses sessions for storing token > extend to your DB of choice
  $storage = new StorageClass();
  $xeroTenantId = (string)$storage->getSession()['tenant_id'];

  if ($storage->getHasExpired()) {
    $provider = new \League\OAuth2\Client\Provider\GenericProvider([
      'clientId'                => 'REMOVED',
      'clientSecret'            => 'REMOVED',
      'redirectUri'             => 'https://REMOVED/callback.php',
      'urlAuthorize'            => 'https://login.xero.com/identity/connect/authorize',
      'urlAccessToken'          => 'https://identity.xero.com/connect/token',
      'urlResourceOwnerDetails' => 'https://api.xero.com/api.xro/2.0/Organisation'
    ]);

    $newAccessToken = $provider->getAccessToken('refresh_token', [
      'refresh_token' => $storage->getRefreshToken()
    ]);

    // Save my token, expiration and refresh token
    $storage->setToken(
        $newAccessToken->getToken(),
        $newAccessToken->getExpires(),
        $xeroTenantId,
        $newAccessToken->getRefreshToken(),
        $newAccessToken->getValues()["id_token"] );
  }

  $config = XeroAPI\XeroPHP\Configuration::getDefaultConfiguration()->setAccessToken( (string)$storage->getSession()['token'] );
  $apiInstance = new XeroAPI\XeroPHP\Api\AccountingApi(
      new GuzzleHttp\Client(),
      $config
  );
  
  
  
// IS THIS THE CUSTOMERS ID E.G. CAN IT BE ANYTHING OR DOES IT NEED TO BE IN e.g. 00000000-0000-0000-0000-000000000000 format?   
$contactId = "00000000-0000-0000-0000-000000000000";
  
// not sure how to specify 3 items in an invoice or if xml is still allowed here??   
$lineitems = "<LineItems>
    <LineItem>
      <Description>Consulting services as agreed (20% off standard rate)</Description>
      <Quantity>10</Quantity>
      <UnitAmount>100.00</UnitAmount>
      <AccountCode>200</AccountCode>
      <DiscountRate>20</DiscountRate>
    </LineItem>
  </LineItems>"; 
  
//start code from   https://github.com/XeroAPI/xero-php-oauth2-app/blob/7a6e4240a24f192d25864ca8ef5ca1573a2a3094/example.php 
  
$contact = new XeroAPI\XeroPHP\Models\Accounting\Contact;
$contact->setContactId($contactId);

$arr_invoices = [];	


$invoice_2 = new XeroAPI\XeroPHP\Models\Accounting\Invoice;
$invoice_2->setReference('Ref-')
	->setDueDate(new DateTime('2019-12-02'))
	->setContact($contact)
	->setLineItems($lineitems)
	->setStatus(XeroAPI\XeroPHP\Models\Accounting\Invoice::STATUS_AUTHORISED)
	->setType(XeroAPI\XeroPHP\Models\Accounting\Invoice::TYPE_ACCPAY)
	->setLineAmountTypes(\XeroAPI\XeroPHP\Models\Accounting\LineAmountTypes::EXCLUSIVE);	
array_push($arr_invoices, $invoice_2);
			
$invoices = new XeroAPI\XeroPHP\Models\Accounting\Invoices;
$invoices->setInvoices($arr_invoices);



//end  code from   https://github.com/XeroAPI/xero-php-oauth2-app/blob/7a6e4240a24f192d25864ca8ef5ca1573a2a3094/example.php 
try {
    $result = $apiInstance->createInvoices($xeroTenantId,$invoices); 
    print_r($result);
} catch (Exception $e) {
    echo 'Exception when calling AccountingApi->createInvoices: ', $e->getMessage(), PHP_EOL;
}


?>

any help would be greatly appreciated.

Any chance you could provide an example of how to pass the minimum details of a product in $lineitems = ""?

Hi @githubjonny, you will need to create a LineItem object and use the setLineItems method on the Invoice object.

If you read the example that @SidneyAllen you should be able to work it out. You'll see that in the example, when they're setting the lineitem on the invoice object, they're calling a mock line item through a getLineItem method.

I've attached a screenshot of that example method.

image

Hello @rodjsta
Thank you for your help.
I've been going over this for the past hour and am still struggling. I think the problem is (other than me!) that oauth1 had a working example (so it could be edited then built upon etc). Oauth2 whilst it does have an example, that example is mixed in with loads of other examples and for someone like myself (who has beginner-ish level / not on same level as you guys/gals! of php understanding), its incredibly hard to work out which bits of code from which lines etc all go together etc.

Oauth1 was perfect from my perspective, e.g. It allowed me to run it locally on WAMP and link to it from my website and the example code was basic enough to allow me to tweak it and to send over invoice details (no other features were needed).

I'll keep hammering away at it and thank you again for your help.

Do you know when Oauth1 is going to be stopped?

The OAuth 2.0 SDK uses a fundamentally different approach, in that you create objects and pass these to the SDK. The SDK then does the work of creating the payload for you (now in JSON format).

To import an invoice you first create an Invoice object and populate it with the various data. This will typically include a Contact object and an array of LineItem objects.

Finally you place the Invoice object into an Array of it's own and pass this to the SDK.

There was a previous issue raised on a similar subject here:
#122

The example code here: https://github.com/XeroAPI/xero-php-oauth2#authorizedresourcephp does the same thing with a contact.

The various get and set functions are named in line with the field names in the documentation here: https://developer.xero.com/documentation/api/invoices
So for the status of the invoice you would use setStatus and so on.

Hello @wobinb

Thank you for your help with this.

I've looked over all the above comments and am trying my best to get my head around it.

I've now got:

$lineitem = new XeroAPI\XeroPHP\Models\Accounting\LineItem;
$lineitem->setDescription('Sample Item')
		->setQuantity(1)
		->setUnitAmount(20)
		->setAccountCode("400");

$arr_lineitem = [];
array_push($arr_lineitem, $lineitem);



//var_dump($arr_lineitem);	
	
	
	

//[Invoices:Create]
$contact = new XeroAPI\XeroPHP\Models\Accounting\Contact;
$contact->setContactId($contactId);

$arr_invoices = [];	

$invoice_1 = new XeroAPI\XeroPHP\Models\Accounting\Invoice;
$invoice_1->setReference('Ref-')
	->setDueDate(new DateTime('2019-12-10'))
	->setContact($contact)
	->setLineItems($arr_lineitem)
	->setStatus(XeroAPI\XeroPHP\Models\Accounting\Invoice::STATUS_AUTHORISED)
	->setType(XeroAPI\XeroPHP\Models\Accounting\Invoice::TYPE_ACCPAY)
	->setLineAmountTypes(\XeroAPI\XeroPHP\Models\Accounting\LineAmountTypes::EXCLUSIVE);	
array_push($arr_invoices, $invoice_1);
	
$invoice_2 = new XeroAPI\XeroPHP\Models\Accounting\Invoice;
$invoice_2->setReference('Ref-')
	->setDueDate(new DateTime('2019-12-02'))
	->setContact($contact)
	->setLineItems($arr_lineitem)
	->setStatus(XeroAPI\XeroPHP\Models\Accounting\Invoice::STATUS_AUTHORISED)
	->setType(XeroAPI\XeroPHP\Models\Accounting\Invoice::TYPE_ACCPAY)
	->setLineAmountTypes(\XeroAPI\XeroPHP\Models\Accounting\LineAmountTypes::EXCLUSIVE);	
array_push($arr_invoices, $invoice_2);
			
$invoices = new XeroAPI\XeroPHP\Models\Accounting\Invoices;
$invoices->setInvoices($arr_invoices);

$result = $apiInstance->createInvoices($xeroTenantId,$invoices); 
//[/Invoices:Create]

which seems to be moving in the right direction.

When I check out https://developer.xero.com/app/apphistory/ I can see in the Request body

{"Invoices":[{"Type":"ACCPAY","Contact":{"HasAttachments":false,"HasValidationErrors":false},"LineItems":{"Description":"Sample Item","Quantity":1,"UnitAmount":20,"AccountCode":"400"},"DueDate":"2019-12-10T00:00:00+00:00","LineAmountTypes":"Exclusive","Reference":"Ref-","Status":"AUTHORISED","HasAttachments":false,"HasErrors":false},{"Type":"ACCPAY","Contact":{"HasAttachments":false,"HasValidationErrors":false},"LineItems":{"Description":"Sample Item","Quantity":1,"UnitAmount":20,"AccountCode":"400"},"DueDate":"2019-12-02T00:00:00+00:00","LineAmountTypes":"Exclusive","Reference":"Ref-","Status":"AUTHORISED","HasAttachments":false,"HasErrors":false}]}

which does not have the invoice details. However, I am getting a status code of 500 so something still is incorrect.

Commenting out the lines:

$invoice_1 = new XeroAPI\XeroPHP\Models\Accounting\Invoice;
$invoice_1->setReference('Ref-')
	->setDueDate(new DateTime('2019-12-10'))
	->setContact($contact)
	->setLineItems($arr_lineitem)
	->setStatus(XeroAPI\XeroPHP\Models\Accounting\Invoice::STATUS_AUTHORISED)
	->setType(XeroAPI\XeroPHP\Models\Accounting\Invoice::TYPE_ACCPAY)
	->setLineAmountTypes(\XeroAPI\XeroPHP\Models\Accounting\LineAmountTypes::EXCLUSIVE);	
array_push($arr_invoices, $invoice_1);

(since I will only ever be submitting 1 invoice at a time) helps and changes it to a "status code 200" but nothing shows up in xero under invoices (so something must still be wrong somewhere).

Looking over the code, I think its contact ID (thats missing causing the problem??). Can this be in any format?

UPDATE :

Changed line
$contact->setContactId($contactId);
to an actual customer (changed to xxx for security)
$contact->setContactId('xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx');
this now gives a status code of 200 and shows in the request body:
{"Invoices":[{"Type":"ACCPAY","Contact":{"ContactID":"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx","HasAttachments":false,"HasValidationErrors":false},"LineItems":[{"Description":"Sample Item","Quantity":1,"UnitAmount":20,"AccountCode":"400"}],"DueDate":"2019-12-02T00:00:00+00:00","LineAmountTypes":"Exclusive","Reference":"Ref-","Status":"AUTHORISED","HasAttachments":false,"HasErrors":false}]}
However, nothing shows in xero under invoices so I'm still missing something.

This is the current full code (minus security keys etc)


<?php
  ini_set('display_errors', 'On');
  require __DIR__ . '/vendor/autoload.php';
  require_once('storage.php');

  // Use this class to deserialize error caught
  use XeroAPI\XeroPHP\AccountingObjectSerializer;

  // Storage Classe uses sessions for storing token > extend to your DB of choice
  $storage = new StorageClass();
  $xeroTenantId = (string)$storage->getSession()['tenant_id'];

  if ($storage->getHasExpired()) {
    $provider = new \League\OAuth2\Client\Provider\GenericProvider([
      'clientId'                => 'REMOVED',
      'clientSecret'            => 'REMOVED',
      'redirectUri'             => 'https://REMOVED/callback.php',
      'urlAuthorize'            => 'https://login.xero.com/identity/connect/authorize',
      'urlAccessToken'          => 'https://identity.xero.com/connect/token',
      'urlResourceOwnerDetails' => 'https://api.xero.com/api.xro/2.0/Organisation'
    ]);

    $newAccessToken = $provider->getAccessToken('refresh_token', [
      'refresh_token' => $storage->getRefreshToken()
    ]);

    // Save my token, expiration and refresh token
    $storage->setToken(
        $newAccessToken->getToken(),
        $newAccessToken->getExpires(),
        $xeroTenantId,
        $newAccessToken->getRefreshToken(),
        $newAccessToken->getValues()["id_token"] );
  }

  $config = XeroAPI\XeroPHP\Configuration::getDefaultConfiguration()->setAccessToken( (string)$storage->getSession()['token'] );
  $apiInstance = new XeroAPI\XeroPHP\Api\AccountingApi(
      new GuzzleHttp\Client(),
      $config
  );
  

$lineitem = new XeroAPI\XeroPHP\Models\Accounting\LineItem;
$lineitem->setDescription('Sample Item')
		->setQuantity(1)
		->setUnitAmount(20)
		->setAccountCode("400");

$arr_lineitem = [];
array_push($arr_lineitem, $lineitem);

//var_dump($arr_lineitem);	
	
//[Invoices:Create]
$contact = new XeroAPI\XeroPHP\Models\Accounting\Contact;
$contact->setContactId('REMOVED aka xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx');

$arr_invoices = [];	

//$invoice_1 = new XeroAPI\XeroPHP\Models\Accounting\Invoice;
//$invoice_1->setReference('Ref-')
//	->setDueDate(new DateTime('2019-12-10'))
//	->setContact($contact)
//	->setLineItems($arr_lineitem)
//	->setStatus(XeroAPI\XeroPHP\Models\Accounting\Invoice::STATUS_AUTHORISED)
//	->setType(XeroAPI\XeroPHP\Models\Accounting\Invoice::TYPE_ACCPAY)
//	->setLineAmountTypes(\XeroAPI\XeroPHP\Models\Accounting\LineAmountTypes::EXCLUSIVE);	
//array_push($arr_invoices, $invoice_1);
	
$invoice_2 = new XeroAPI\XeroPHP\Models\Accounting\Invoice;
$invoice_2->setReference('Ref-')
	->setDueDate(new DateTime('2019-12-02'))
	->setContact($contact)
	->setLineItems($arr_lineitem)
	->setStatus(XeroAPI\XeroPHP\Models\Accounting\Invoice::STATUS_AUTHORISED)
	->setType(XeroAPI\XeroPHP\Models\Accounting\Invoice::TYPE_ACCPAY)
	->setLineAmountTypes(\XeroAPI\XeroPHP\Models\Accounting\LineAmountTypes::EXCLUSIVE);	
array_push($arr_invoices, $invoice_2);
			
$invoices = new XeroAPI\XeroPHP\Models\Accounting\Invoices;
$invoices->setInvoices($arr_invoices);


//[/Invoices:Create]


try {
    $result = $apiInstance->createInvoices($xeroTenantId,$invoices); 
    print_r($result);
} catch (Exception $e) {
    echo 'Exception when calling AccountingApi->createInvoices: ', $e->getMessage(), PHP_EOL;
}

?>

and the output of getMessage():
XeroAPI\XeroPHP\Models\Accounting\Invoices Object ( [container:protected] => Array ( [invoices] => Array ( [0] => XeroAPI\XeroPHP\Models\Accounting\Invoice Object ( [container:protected] => Array ( [type] => ACCPAY [contact] => XeroAPI\XeroPHP\Models\Accounting\Contact Object ( [container:protected] => Array ( [contact_id] => xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx [contact_number] => [account_number] => [contact_status] => ACTIVE [name] => David Camerotto [first_name] => [last_name] => [email_address] => [skype_user_name] => [contact_persons] => Array ( ) [bank_account_details] => [tax_number] => [accounts_receivable_tax_type] => [accounts_payable_tax_type] => [addresses] => Array ( [0] => XeroAPI\XeroPHP\Models\Accounting\Address Object ( [container:protected] => Array ( [address_type] => STREET [address_line1] => [address_line2] => [address_line3] => [address_line4] => [city] => [region] => [postal_code] => [country] => [attention_to] => ) ) [1] => XeroAPI\XeroPHP\Models\Accounting\Address Object ( [container:protected] => Array ( [address_type] => POBOX [address_line1] => [address_line2] => [address_line3] => [address_line4] => [city] => [region] => [postal_code] => [country] => [attention_to] => ) ) ) [phones] => Array ( [0] => XeroAPI\XeroPHP\Models\Accounting\Phone Object ( [container:protected] => Array ( [phone_type] => DEFAULT [phone_number] => [phone_area_code] => [phone_country_code] => ) ) [1] => XeroAPI\XeroPHP\Models\Accounting\Phone Object ( [container:protected] => Array ( [phone_type] => DDI [phone_number] => [phone_area_code] => [phone_country_code] => ) ) [2] => XeroAPI\XeroPHP\Models\Accounting\Phone Object ( [container:protected] => Array ( [phone_type] => FAX [phone_number] => [phone_area_code] => [phone_country_code] => ) ) [3] => XeroAPI\XeroPHP\Models\Accounting\Phone Object ( [container:protected] => Array ( [phone_type] => MOBILE [phone_number] => [phone_area_code] => [phone_country_code] => ) ) ) [is_supplier] => 1 [is_customer] => 1 [default_currency] => [xero_network_key] => [sales_default_account_code] => [purchases_default_account_code] => [sales_tracking_categories] => Array ( ) [purchases_tracking_categories] => Array ( ) [tracking_category_name] => [tracking_category_option] => [payment_terms] => [updated_date_utc] => /Date(1607613226810+0000)/ [contact_groups] => Array ( ) [website] => [branding_theme] => [batch_payments] => [discount] => [balances] => [attachments] => [has_attachments] => [validation_errors] => [has_validation_errors] => [status_attribute_string] => ) ) [line_items] => Array ( [0] => XeroAPI\XeroPHP\Models\Accounting\LineItem Object ( [container:protected] => Array ( [line_item_id] => 295c5641-605c-42f9-9f8d-56f2e6400bcf [description] => Sample Item [quantity] => 1 [unit_amount] => 20 [item_code] => [account_code] => 400 [tax_type] => INPUT2 [tax_amount] => 4 [line_amount] => 20 [tracking] => Array ( ) [discount_rate] => [discount_amount] => [repeating_invoice_id] => ) ) ) [date] => /Date(1607644800000+0000)/ [due_date] => /Date(1575244800000+0000)/ [line_amount_types] => Exclusive [invoice_number] => [reference] => Ref- [branding_theme_id] => 6f857b81-25b2-4039-a4fe-4a1167cd9c5c [url] => [currency_code] => GBP [currency_rate] => 1 [status] => AUTHORISED [sent_to_contact] => [expected_payment_date] => [planned_payment_date] => [cis_deduction] => [sub_total] => 20 [total_tax] => 4 [total] => 24 [total_discount] => [invoice_id] => c692c516-de45-4838-a618-0edb769db392 [has_attachments] => [is_discounted] => [payments] => [prepayments] => Array ( ) [overpayments] => Array ( ) [amount_due] => 24 [amount_paid] => 0 [fully_paid_on_date] => [amount_credited] => [updated_date_utc] => /Date(1607687725170+0000)/ [credit_notes] => [attachments] => [has_errors] => [status_attribute_string] => OK [validation_errors] => [warnings] => ) ) ) ) )

Any idea what I'm missing? Not getting any error messages and status code is 200 but nothing showing in xero -> invoices so something isn't correct somewhere.

Any way to get any additional error messages?

ok, just spotted it shows up as a "bill" in xero under the customer account instead of under invoices.

instead of under

ACCREC!!!!!!

So I now have invoices in drafts :)

for anyone else this is what I then changed to get it in invoices:

	->setStatus(XeroAPI\XeroPHP\Models\Accounting\Invoice::STATUS_DRAFT)
	->setType(XeroAPI\XeroPHP\Models\Accounting\Invoice::TYPE_ACCREC)
	->setLineAmountTypes(\XeroAPI\XeroPHP\Models\Accounting\LineAmountTypes::EXCLUSIVE);

Pretty sure this isn't a bug and just me not fully understanding it but the code:


$setFirstName = "James";
$setLastName = "Price";
$setEmailAddress = "test@tests.com";

$contact = new XeroAPI\XeroPHP\Models\Accounting\Contact;
            $contact->setName($setFirstName . $setLastName)
                ->setFirstName($setFirstName)
                ->setLastName($setLastName)
                ->setEmailAddress($setEmailAddress);
                
            
            $arr_contacts = [];
            array_push($arr_contacts, $contact);
            
            $contacts = new XeroAPI\XeroPHP\Models\Accounting\Contacts;
            $contacts->setContacts($arr_contacts);

            $apiResponse = $apiInstance->createContacts($xeroTenantId,$contacts);
            $message = 'New Contact Name: ' . $apiResponse->getContacts()[0]->getName() .'<hr>' . $apiResponse->getContacts()[0]->getContactId() ;

echo $message;

will create a contact.

However, if I change everything but the name (so if someone else with the same name orders), it overwrites the original user with the new details.

Each of my customers on my website has a unique ID, is there a way to use this with xero instead of the xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx id?

update;
just spotted "ContactNumber" would fit this. Looking for correct usage. Thought it would be:

->setContactNumber($var)

but this doesn't seem to work as in it just updates the customer to use the 'new' contact number (instead of creating a new customer)

Full code below:

// Define the vars
$setFirstName = "James";
$setLastName = "Price";
$setEmailAddress = "test@tests.com";
$setContactNumber = "13";

//[add a contact]
            $contact = new XeroAPI\XeroPHP\Models\Accounting\Contact;
            $contact->setName($setFirstName . $setLastName)
                ->setFirstName($setFirstName)
                ->setLastName($setLastName)
                ->setEmailAddress($setEmailAddress)
                ->setContactNumber($setContactNumber);
          
            $arr_contacts = [];
            array_push($arr_contacts, $contact);
            
            $contacts = new XeroAPI\XeroPHP\Models\Accounting\Contacts;
            $contacts->setContacts($arr_contacts);

            $apiResponse = $apiInstance->createContacts($xeroTenantId,$contacts);
            
            $message = 'New Contact Name: ' . $apiResponse->getContacts()[0]->getName() .'<hr>' . $apiResponse->getContacts()[0]->getContactId() ;
            
            echo "<hr>";
            echo $message;
            echo "<hr>";

//[/add a contact]

Can you see why this is not creating a new contact but instead overwriting the existing contact with the same name? Looking over example.php it would appear "$apiInstance->createContacts" would create a contact and "$apiInstance->updateOrCreateContacts" would be the update contact.

On https://developer.xero.com/documentation/api/contacts it says

POST Contacts
Use this method to create or update one or more contact records
When you are updating a contact you don’t need to specify every element. If you exclude an element then the existing value will be preserved.

If this is the case, how to I create a new contact with the same name? e.g. two different contacts with the name "John Doe"? I could add a unique number to the last name but this is not ideal as it would show up on the invoices etc as "John Does 10023".

Hey @githubjonny nice work persisting through. Unfortunately Xero also requires Contact Name to be unique. You'll notice in the UI this is also the case. If you do some searching you'll also find many others who have complained before you. Xero have also said they're looking at making some changes to contacts in the future. I manage contacts (and other objects) by saving the Xero guid on my end.

In another application I've also appended an external ID number to the contact name if that name already exists in Xero. It seems like a very stupid limitation on Xero's behalf. I can't believe they're the size they are and still have this restriction.

Thanks @rodjsta for jumping in to help 👍

Can someone please try help me, I have spent hours trying to get this right, now out of ideas.

I use all the samples, when I run it I get this error

I have managed to create a new contact so my link to Xero is working fine, and the api seems to be working okay.

Call to undefined method XeroPHP\Models\Accounting\Invoice::setLineItems()

I have searched my src folder for the term setlineitems and its nowhere in the src code even.

here is my code to create a sample draft invvoice:

$jobdescription = 'test line item 1';
$quantity = 5;
$rateperhour = 150.10;
$accountcode = '1120';
  
$contact = new \XeroPHP\Models\Accounting\Contact;
$contact->setContactId($contactid);

$lineitem = new \XeroPHP\Models\Accounting\LineItem;
$lineitem->setDescription($jobdescription)
	->setQuantity($quantity)
	->setUnitAmount(number_format($rateperhour, 2))
	->setAccountCode($accountcode);

$arr_lineitems=[]; 

array_push($arr_lineitems, $lineitem);

$invoice = new \XeroPHP\Models\Accounting\Invoice($xero);

$invoice->setContact($contact)	;	
$invoice->setLineItems($lineitem);
$invoice->save();