fjffk简å�•的咖啡店科开å�‘2方法2 环æ»�皆山也。其西å�—诸峰,林壑尤美,望之蔚然而深秀者,ç�…ç�Šä¹Ÿã€‚山行六七里,æ¸�闻水声潺潺,而泻出于两峰之间者,酿泉也 ??????????????????????????????? ??????????????/????????????????? ............/................... .........................../... ??????????????????????????????? >>>>>>>>>>>>>/>>>>>>>>>>>>>>>>> ÿØÿà JFIF    ÿÛ „  ( %!1!%)+//.383,7(-.+  -%%-////---/-.+/--+------/------/--0+--/-/-----.-----ÿÀ  Â¥2" ÿÄ     ÿÄ J    ! 1AQ"aqÂ�2‘#BR‚¡Ã�Ñ3br’¢±Âð$CSƒ²á4c“%DsÓñÿÄ   ÿÄ *  !1AQa‘"2q3Â�±ð#b¡ÿÚ   ? ¼QxJQaÃ�uò¸Zö Úü8,Ã�Úú "SSn<rçù–´âE—^ªBÖ9À\†¸Â�ÔÂ�Ã�T­ÃÛ5 ëd´³Ã�#Ã�;Þ38ϔÂ� ¶H£M:wÃŽ3…³…âpÔF&‚FK¸9„â4àGEõªfÂ�ÿ ‘ñ(ßw­pŽF|È¥ù®häðÃ�Ѷ¹‘[Ã’inÙW¶ùñYËœQ{›K"išÒ[Ú8žë\F¹@-?v"ÔUâ€�,ìöžkÿ {I‡£šÃ�?e rÂ�íV ?> ......................................... ............................................................................. <<<<<<<<<<<<<<<<<<<<<<<<<<<<<< >>>>>>>>>>>>>/>>>>>>>>>>>>>>>>>> <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< >>>>>>>>>>>>>>>>>>>>>>>>>>>>>> dsdfPKºqW]Ÿ„¾¥==laravel-xero/readme.mdnu„[µü¤## Community There is a Discord community. https://discord.gg/VYau8hgwrm For quick help, ask questions in the appropriate channel. [![Latest Version on Packagist](https://img.shields.io/packagist/v/dcblogdev/laravel-xero.svg?style=flat-square)](https://packagist.org/packages/dcblogdev/laravel-xero) [![Total Downloads](https://img.shields.io/packagist/dt/dcblogdev/laravel-xero.svg?style=flat-square)](https://packagist.org/packages/dcblogdev/laravel-xero) ![Logo](https://repository-images.githubusercontent.com/317929912/1e40a180-49c1-11eb-893d-af9c59d29ad5) Laravel package for working with the Xero API Watch a video walkthrough https://www.youtube.com/watch?v=sORX2z-AH1k Xero API documentation can be found at: https://developer.xero.com/documentation/ Before you can integrate with Xero you will need to create an app, go to https://developer.xero.com/myapps to register a new app. For the grant type select, Auth code (web app) For OAuth 2.0 redirect URI enter the full URL you want to use for connection to Xero from your application such as https://domain.com/xero/connect ## Documentation Complete docs at https://dcblog.dev/docs/laravel-xero/v1/introduction ## Change log Please see the [changelog][3] for more information on what has changed recently. ## Contributing Contributions are welcome and will be fully credited. Contributions are accepted via Pull Requests on [Github][4]. ## Pull Requests - **Document any change in behaviour** - Make sure the `readme.md` and any other relevant documentation are kept up-to-date. - **Consider our release cycle** - We try to follow [SemVer v2.0.0][5]. Randomly breaking public APIs is not an option. - **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. ## Security If you discover any security related issues, please email dave@dcblog.dev email instead of using the issue tracker. ## License license. Please see the [license file][6] for more information. [3]: changelog.md [4]: https://github.com/dcblogdev/laravel-xero [5]: http://semver.org/ [6]: license.md PKºqW]Â:4N¼¼*laravel-xero/tests/DTOs/InvoiceDTOTest.phpnu„[µü¤toBeInstanceOf(InvoiceDTO::class) ->and($invoiceDTO->invoiceID)->toBeNull() ->and($invoiceDTO->type)->toBe(InvoiceType::AccRec->value) ->and($invoiceDTO->invoiceNumber)->toBeNull() ->and($invoiceDTO->reference)->toBeNull() ->and($invoiceDTO->date)->toBeNull() ->and($invoiceDTO->dueDate)->toBeNull() ->and($invoiceDTO->status)->toBe(InvoiceStatus::Draft->value) ->and($invoiceDTO->lineAmountTypes)->toBe(InvoiceLineAmountType::Exclusive->value) ->and($invoiceDTO->currencyCode)->toBeNull() ->and($invoiceDTO->currencyRate)->toBeNull() ->and($invoiceDTO->subTotal)->toBeNull() ->and($invoiceDTO->totalTax)->toBeNull() ->and($invoiceDTO->total)->toBeNull() ->and($invoiceDTO->contactID)->toBeNull() ->and($invoiceDTO->contact)->toBeNull() ->and($invoiceDTO->lineItems)->toBe([]) ->and($invoiceDTO->payments)->toBe([]) ->and($invoiceDTO->creditNotes)->toBe([]) ->and($invoiceDTO->prepayments)->toBe([]) ->and($invoiceDTO->overpayments)->toBe([]) ->and($invoiceDTO->hasAttachments)->toBeFalse() ->and($invoiceDTO->isDiscounted)->toBeFalse() ->and($invoiceDTO->hasErrors)->toBeFalse(); }); test('invoice dto can be instantiated with custom values', function () { $invoiceDTO = new InvoiceDTO( invoiceID: 'INV-123', type: InvoiceType::AccPay->value, invoiceNumber: '123', reference: 'REF-123', date: '2023-01-01', dueDate: '2023-01-31', status: InvoiceStatus::Submitted->value, lineAmountTypes: InvoiceLineAmountType::Inclusive->value, currencyCode: 'USD', currencyRate: '1.0', subTotal: '100.00', totalTax: '10.00', total: '110.00', contactID: 'CONTACT-123', contact: [['ContactID' => 'CONTACT-123', 'Name' => 'Test Contact']], lineItems: [['Description' => 'Test Item']], payments: [['PaymentID' => 'PAYMENT-123']], creditNotes: [['CreditNoteID' => 'CREDIT-123']], prepayments: [['PrepaymentID' => 'PREPAY-123']], overpayments: [['OverpaymentID' => 'OVERPAY-123']], hasAttachments: true, isDiscounted: true, hasErrors: true ); expect($invoiceDTO)->toBeInstanceOf(InvoiceDTO::class) ->and($invoiceDTO->invoiceID)->toBe('INV-123') ->and($invoiceDTO->type)->toBe(InvoiceType::AccPay->value) ->and($invoiceDTO->invoiceNumber)->toBe('123') ->and($invoiceDTO->reference)->toBe('REF-123') ->and($invoiceDTO->date)->toBe('2023-01-01') ->and($invoiceDTO->dueDate)->toBe('2023-01-31') ->and($invoiceDTO->status)->toBe(InvoiceStatus::Submitted->value) ->and($invoiceDTO->lineAmountTypes)->toBe(InvoiceLineAmountType::Inclusive->value) ->and($invoiceDTO->currencyCode)->toBe('USD') ->and($invoiceDTO->currencyRate)->toBe('1.0') ->and($invoiceDTO->subTotal)->toBe('100.00') ->and($invoiceDTO->totalTax)->toBe('10.00') ->and($invoiceDTO->total)->toBe('110.00') ->and($invoiceDTO->contactID)->toBe('CONTACT-123') ->and($invoiceDTO->contact)->toBe([['ContactID' => 'CONTACT-123', 'Name' => 'Test Contact']]) ->and($invoiceDTO->lineItems)->toBe([['Description' => 'Test Item']]) ->and($invoiceDTO->payments)->toBe([['PaymentID' => 'PAYMENT-123']]) ->and($invoiceDTO->creditNotes)->toBe([['CreditNoteID' => 'CREDIT-123']]) ->and($invoiceDTO->prepayments)->toBe([['PrepaymentID' => 'PREPAY-123']]) ->and($invoiceDTO->overpayments)->toBe([['OverpaymentID' => 'OVERPAY-123']]) ->and($invoiceDTO->hasAttachments)->toBeTrue() ->and($invoiceDTO->isDiscounted)->toBeTrue() ->and($invoiceDTO->hasErrors)->toBeTrue(); }); test('createLineItem static method returns correct array with all values', function () { $lineItem = InvoiceDTO::createLineItem( description: 'Test Item', quantity: 2, unitAmount: 10.50, accountCode: 123, itemCode: 'ITEM-123', taxType: 'OUTPUT', taxAmount: '2.10', lineAmount: '21.00', discountRate: '10', tracking: [['Name' => 'Region', 'Option' => 'North']] ); expect($lineItem)->toBe([ 'Description' => 'Test Item', 'Quantity' => 2, 'UnitAmount' => 10.50, 'AccountCode' => 123, 'ItemCode' => 'ITEM-123', 'TaxType' => 'OUTPUT', 'TaxAmount' => '2.10', 'LineAmount' => '21.00', 'DiscountRate' => '10', 'Tracking' => [['Name' => 'Region', 'Option' => 'North']], ]); }); test('createLineItem static method filters out null values', function () { $lineItem = InvoiceDTO::createLineItem( description: 'Test Item', quantity: 2, unitAmount: 10.50, accountCode: null, itemCode: null ); expect($lineItem)->toBe([ 'Description' => 'Test Item', 'Quantity' => 2, 'UnitAmount' => 10.50, ]); }); test('createLineItem accepts string values for numeric fields', function () { $lineItem = InvoiceDTO::createLineItem( description: 'Test Item', quantity: '2', unitAmount: '10.50' ); expect($lineItem)->toBe([ 'Description' => 'Test Item', 'Quantity' => '2', 'UnitAmount' => '10.50', ]); }); test('toArray method returns correct array structure', function () { $invoiceDTO = new InvoiceDTO( type: InvoiceType::AccRec->value, invoiceNumber: '123', date: '2023-01-01', dueDate: '2023-01-31', status: InvoiceStatus::Draft->value, contactID: 'CONTACT-123', lineItems: [ InvoiceDTO::createLineItem( description: 'Test Item', quantity: 2, unitAmount: 10.50 ), ] ); $array = $invoiceDTO->toArray(); expect($array)->toBeArray() ->and($array)->toHaveKey('Type', InvoiceType::AccRec->value) ->and($array)->toHaveKey('InvoiceNumber', '123') ->and($array)->toHaveKey('Date', '2023-01-01') ->and($array)->toHaveKey('DueDate', '2023-01-31') ->and($array)->toHaveKey('Status', InvoiceStatus::Draft->value) ->and($array)->toHaveKey('LineAmountTypes', InvoiceLineAmountType::Exclusive->value) ->and($array)->toHaveKey('Contact') ->and($array['Contact'])->toBe(['ContactID' => 'CONTACT-123']) ->and($array)->toHaveKey('LineItems') ->and($array['LineItems'][0])->toHaveKey('Description', 'Test Item') ->and($array['LineItems'][0])->toHaveKey('Quantity', 2) ->and($array['LineItems'][0])->toHaveKey('UnitAmount', 10.50); }); test('toArray method uses contact array when contactID is null', function () { $invoiceDTO = new InvoiceDTO( contact: [ 'ContactID' => 'CONTACT-123', 'Name' => 'Test Contact', ] ); $array = $invoiceDTO->toArray(); expect($array)->toHaveKey('Contact') ->and($array['Contact'])->toBe([ 'ContactID' => 'CONTACT-123', 'Name' => 'Test Contact', ]); }); test('toArray method filters out null and empty array values', function () { $invoiceDTO = new InvoiceDTO( invoiceNumber: '123', reference: null, lineItems: [] ); $array = $invoiceDTO->toArray(); expect($array)->toHaveKey('InvoiceNumber') ->and($array)->toHaveKey('Type') ->and($array)->toHaveKey('Status') ->and($array)->toHaveKey('LineAmountTypes') ->and($array)->not->toHaveKey('Reference') ->and($array)->not->toHaveKey('LineItems'); }); PKºqW]µè¸c*laravel-xero/tests/DTOs/ContactDTOTest.phpnu„[µü¤toBeInstanceOf(ContactDTO::class) ->and($contactDTO->name)->toBeNull() ->and($contactDTO->firstName)->toBeNull() ->and($contactDTO->lastName)->toBeNull() ->and($contactDTO->emailAddress)->toBeNull() ->and($contactDTO->accountNumber)->toBeNull() ->and($contactDTO->bankAccountDetails)->toBeNull() ->and($contactDTO->taxNumber)->toBeNull() ->and($contactDTO->accountsReceivableTaxType)->toBeNull() ->and($contactDTO->accountsPayableTaxType)->toBeNull() ->and($contactDTO->contactStatus)->toBe(ContactStatus::Active->value) ->and($contactDTO->isSupplier)->toBeFalse() ->and($contactDTO->isCustomer)->toBeFalse() ->and($contactDTO->defaultCurrency)->toBeNull() ->and($contactDTO->website)->toBeNull() ->and($contactDTO->purchasesDefaultAccountCode)->toBeNull() ->and($contactDTO->salesDefaultAccountCode)->toBeNull() ->and($contactDTO->addresses)->toBe([]) ->and($contactDTO->phones)->toBe([]) ->and($contactDTO->contactPersons)->toBe([]) ->and($contactDTO->hasAttachments)->toBeFalse() ->and($contactDTO->hasValidationErrors)->toBeFalse(); }); test('contact dto can be instantiated with custom values', function () { $contactDTO = new ContactDTO( name: 'Test Company', firstName: 'John', lastName: 'Doe', emailAddress: 'john.doe@example.com', accountNumber: 'ACC123', bankAccountDetails: '123456789', taxNumber: 'TAX123', accountsReceivableTaxType: 'OUTPUT', accountsPayableTaxType: 'INPUT', contactStatus: ContactStatus::Archived->value, isSupplier: true, isCustomer: true, defaultCurrency: 'USD', website: 'https://example.com', purchasesDefaultAccountCode: 'P123', salesDefaultAccountCode: 'S123', addresses: [['AddressType' => 'POBOX']], phones: [['PhoneType' => 'MOBILE']], contactPersons: [['FirstName' => 'Jane']], hasAttachments: true, hasValidationErrors: true ); expect($contactDTO)->toBeInstanceOf(ContactDTO::class) ->and($contactDTO->name)->toBe('Test Company') ->and($contactDTO->firstName)->toBe('John') ->and($contactDTO->lastName)->toBe('Doe') ->and($contactDTO->emailAddress)->toBe('john.doe@example.com') ->and($contactDTO->accountNumber)->toBe('ACC123') ->and($contactDTO->bankAccountDetails)->toBe('123456789') ->and($contactDTO->taxNumber)->toBe('TAX123') ->and($contactDTO->accountsReceivableTaxType)->toBe('OUTPUT') ->and($contactDTO->accountsPayableTaxType)->toBe('INPUT') ->and($contactDTO->contactStatus)->toBe(ContactStatus::Archived->value) ->and($contactDTO->isSupplier)->toBeTrue() ->and($contactDTO->isCustomer)->toBeTrue() ->and($contactDTO->defaultCurrency)->toBe('USD') ->and($contactDTO->website)->toBe('https://example.com') ->and($contactDTO->purchasesDefaultAccountCode)->toBe('P123') ->and($contactDTO->salesDefaultAccountCode)->toBe('S123') ->and($contactDTO->addresses)->toBe([['AddressType' => 'POBOX']]) ->and($contactDTO->phones)->toBe([['PhoneType' => 'MOBILE']]) ->and($contactDTO->contactPersons)->toBe([['FirstName' => 'Jane']]) ->and($contactDTO->hasAttachments)->toBeTrue() ->and($contactDTO->hasValidationErrors)->toBeTrue(); }); test('createAddress static method returns correct array', function () { $address = ContactDTO::createAddress( addressType: 'POBOX', addressLine1: '123 Main St', addressLine2: 'Suite 100', addressLine3: 'Building A', addressLine4: 'Floor 2', city: 'Anytown', region: 'State', postalCode: '12345', country: 'USA', attentionTo: 'John Doe' ); expect($address)->toBe([ 'AddressType' => 'POBOX', 'AddressLine1' => '123 Main St', 'AddressLine2' => 'Suite 100', 'AddressLine3' => 'Building A', 'AddressLine4' => 'Floor 2', 'City' => 'Anytown', 'Region' => 'State', 'PostalCode' => '12345', 'Country' => 'USA', 'AttentionTo' => 'John Doe', ]); }); test('createPhone static method returns correct array', function () { $phone = ContactDTO::createPhone( phoneType: 'MOBILE', phoneNumber: '555-1234', phoneAreaCode: '123', phoneCountryCode: '1' ); expect($phone)->toBe([ 'PhoneType' => 'MOBILE', 'PhoneNumber' => '555-1234', 'PhoneAreaCode' => '123', 'PhoneCountryCode' => '1', ]); }); test('createContactPerson static method returns correct array', function () { $contactPerson = ContactDTO::createContactPerson( firstName: 'Jane', lastName: 'Smith', emailAddress: 'jane.smith@example.com', includeInEmails: true ); expect($contactPerson)->toBe([ 'FirstName' => 'Jane', 'LastName' => 'Smith', 'EmailAddress' => 'jane.smith@example.com', 'IncludeInEmails' => true, ]); }); test('toArray method returns correct array structure', function () { $contactDTO = new ContactDTO( name: 'Test Company', firstName: 'John', lastName: 'Doe', emailAddress: 'john.doe@example.com', isSupplier: true, addresses: [ ContactDTO::createAddress('POBOX', '123 Main St'), ], phones: [ ContactDTO::createPhone('MOBILE', '555-1234'), ], contactPersons: [ ContactDTO::createContactPerson('Jane', 'Smith'), ] ); $array = $contactDTO->toArray(); expect($array)->toBeArray() ->and($array)->toHaveKey('Name', 'Test Company') ->and($array)->toHaveKey('FirstName', 'John') ->and($array)->toHaveKey('LastName', 'Doe') ->and($array)->toHaveKey('EmailAddress', 'john.doe@example.com') ->and($array)->toHaveKey('ContactStatus', ContactStatus::Active->value) ->and($array)->toHaveKey('IsSupplier', true) ->and($array)->toHaveKey('IsCustomer', false) ->and($array)->toHaveKey('Addresses') ->and($array['Addresses'][0])->toHaveKey('AddressType', 'POBOX') ->and($array['Addresses'][0])->toHaveKey('AddressLine1', '123 Main St') ->and($array)->toHaveKey('Phones') ->and($array['Phones'][0])->toHaveKey('PhoneType', 'MOBILE') ->and($array['Phones'][0])->toHaveKey('PhoneNumber', '555-1234') ->and($array)->toHaveKey('ContactPersons') ->and($array['ContactPersons'][0])->toHaveKey('FirstName', 'Jane') ->and($array['ContactPersons'][0])->toHaveKey('LastName', 'Smith'); }); test('toArray method filters out null values', function () { $contactDTO = new ContactDTO( name: 'Test Company', emailAddress: null, website: null ); $array = $contactDTO->toArray(); expect($array)->toHaveKey('Name') ->and($array)->not->toHaveKey('EmailAddress') ->and($array)->not->toHaveKey('Website'); }); PKºqW]ü°U¡¡,laravel-xero/tests/Enums/InvoiceTypeTest.phpnu„[µü¤�rsââ.laravel-xero/tests/Enums/ContactStatusTest.phpnu„[µü¤create(); expect($token)->toBeInstanceOf(XeroToken::class) ->and($token->tenant_id)->not->toBeEmpty() ->and($token->tenant_name)->not->toBeEmpty() ->and($token->access_token)->not->toBeEmpty() ->and($token->refresh_token)->not->toBeEmpty() ->and($token->expires_in)->toBeInt(); }); test('xero token can be created with mass assignment', function () { $data = [ 'tenant_id' => 'test_tenant_id', 'tenant_name' => 'Test Tenant', 'access_token' => 'test_access_token', 'refresh_token' => 'test_refresh_token', 'expires_in' => 3600, 'scopes' => 'test_scope', ]; $token = XeroToken::create($data); expect($token)->toBeInstanceOf(XeroToken::class) ->and($token->tenant_id)->toBe('test_tenant_id') ->and($token->tenant_name)->toBe('Test Tenant') ->and($token->access_token)->toBe('test_access_token') ->and($token->refresh_token)->toBe('test_refresh_token') ->and($token->expires_in)->toBe(3600) ->and($token->scopes)->toBe('test_scope'); }); test('expires attribute returns correct expiration time', function () { // Freeze time for predictable testing Carbon::setTestNow('2023-01-01 12:00:00'); $token = XeroToken::create([ 'tenant_id' => 'test_tenant_id', 'access_token' => 'test_access_token', 'refresh_token' => 'test_refresh_token', 'expires_in' => 3600, // 1 hour 'scopes' => 'test_scope', ]); // The expires attribute should be 1 hour after the updated_at timestamp expect($token->expires->format('Y-m-d H:i:s'))->toBe('2023-01-01 13:00:00'); // Clean up Carbon::setTestNow(); }); test('expires_in is cast to integer', function () { $token = XeroToken::create([ 'tenant_id' => 'test_tenant_id', 'access_token' => 'test_access_token', 'refresh_token' => 'test_refresh_token', 'expires_in' => '3600', // String value 'scopes' => 'test_scope', ]); expect($token->expires_in)->toBeInt() ->and($token->expires_in)->toBe(3600); }); PKºqW]Í<*íílaravel-xero/tests/TestCase.phpnu„[µü¤set('database.default', 'mysql'); $app['config']->set('database.connections.mysql', [ 'driver' => 'sqlite', 'database' => ':memory:', 'prefix' => '', ]); require_once 'src/database/migrations/create_xero_tokens_table.php'; // run the up() method of that migration class (new CreateXeroTokensTable)->up(); } } PKºqW]l.ëâ â 3laravel-xero/tests/Actions/StoreTokenActionTest.phpnu„[µü¤ 'test_id_token', 'access_token' => 'test_access_token', 'expires_in' => 3600, 'token_type' => 'Bearer', 'refresh_token' => 'test_refresh_token', 'scope' => 'test_scope', ]; $result = app(StoreTokenAction::class)($token); expect($result)->toBeInstanceOf(XeroToken::class); assertDatabaseCount(XeroToken::class, 1); assertDatabaseHas('xero_tokens', [ 'id' => 1, 'access_token' => config('xero.encrypt') ? null : 'test_access_token', 'refresh_token' => config('xero.encrypt') ? null : 'test_refresh_token', 'scopes' => 'test_scope', ]); }); test('store token action creates a new token with tenant data', function () { $token = [ 'id_token' => 'test_id_token', 'access_token' => 'test_access_token', 'expires_in' => 3600, 'token_type' => 'Bearer', 'refresh_token' => 'test_refresh_token', 'scope' => 'test_scope', ]; $tenantData = [ 'tenant_id' => 'test_tenant_id', 'tenant_name' => 'Test Tenant', ]; $result = app(StoreTokenAction::class)($token, $tenantData); expect($result)->toBeInstanceOf(XeroToken::class); assertDatabaseCount(XeroToken::class, 1); assertDatabaseHas('xero_tokens', [ 'tenant_id' => 'test_tenant_id', 'tenant_name' => 'Test Tenant', 'access_token' => config('xero.encrypt') ? null : 'test_access_token', 'refresh_token' => config('xero.encrypt') ? null : 'test_refresh_token', 'scopes' => 'test_scope', ]); }); test('store token action updates an existing token with tenant id and tenant data', function () { // First, create a token $token = XeroToken::factory()->create([ 'tenant_id' => 'test_tenant_id', 'tenant_name' => 'Old Tenant Name', ]); // Then update it with new data $newToken = [ 'id_token' => 'new_id_token', 'access_token' => 'new_access_token', 'expires_in' => 7200, 'token_type' => 'Bearer', 'refresh_token' => 'new_refresh_token', 'scope' => 'new_scope', ]; $tenantData = [ 'tenant_name' => 'New Tenant Name', ]; $result = app(StoreTokenAction::class)($newToken, $tenantData, 'test_tenant_id'); expect($result)->toBeInstanceOf(XeroToken::class); assertDatabaseCount(XeroToken::class, 1); assertDatabaseHas('xero_tokens', [ 'tenant_id' => 'test_tenant_id', 'tenant_name' => 'New Tenant Name', 'access_token' => config('xero.encrypt') ? null : 'new_access_token', 'refresh_token' => config('xero.encrypt') ? null : 'new_refresh_token', 'scopes' => 'new_scope', ]); }); PKºqW]Ë]¼‹‹;laravel-xero/tests/Actions/formatQueryStringsActionTest.phpnu„[µü¤ 'value1', 'key2' => 'value2', 'key3' => 'value3', ]; $response = app(formatQueryStringsAction::class)($params); expect($response)->toBe('key1=value1&key2=value2&key3=value3'); }); test('throws type error when non-array is passed', function ($value) { $this->expectException(TypeError::class); app(formatQueryStringsAction::class)($value); })->with([ 'string', 123, 1.23, true, false, null, new stdClass(), ]); PKºqW]–:b‰  ;laravel-xero/tests/Actions/HandleTokenExpiredActionTest.phpnu„[µü¤create(); $result = ['error' => 'invalid_grant']; app(tokenExpiredAction::class)($result, $token); assertDatabaseCount(XeroToken::class, 0); })->throws(Exception::class, 'Xero token has expired, please re-authenticate.'); test('token refresh does not throw an exception and token is not deleted', function () { $token = XeroToken::factory()->create(); $result = []; $response = app(tokenExpiredAction::class)($result, $token); expect($response)->toBeNull(); assertDatabaseCount(XeroToken::class, 1); }); test('token refresh redirects when expired and not running in console', function () { // Create a test-specific version of the tokenExpiredAction class // that overrides isRunningInConsole to return false $testAction = new class extends tokenExpiredAction { protected function isRunningInConsole(): bool { return false; } }; // Set a test redirect URI Config::set('xero.redirectUri', 'https://example.com/auth'); $token = XeroToken::factory()->create(); $result = ['error' => 'invalid_grant']; $response = $testAction($result, $token); // Assert token was deleted assertDatabaseCount(XeroToken::class, 0); // Assert response is a redirect to the configured URI expect($response)->toBeInstanceOf(RedirectResponse::class); expect($response->getTargetUrl())->toBe('https://example.com/auth'); }); PKºqW]èìè8iilaravel-xero/tests/Pest.phpnu„[µü¤in(__DIR__); PKºqW]žü&ï� � Dlaravel-xero/tests/Console/Commands/XeroShowAllTokensCommandTest.phpnu„[µü¤app->singleton('command.xero.show-all-tokens', fn () => new XeroShowAllTokensCommand()); Artisan::registerCommand($this->app->make('command.xero.show-all-tokens')); }); test('command displays message when no tokens exist', function () { // Ensure no tokens exist XeroToken::query()->delete(); // Run the command $this->artisan('xero:show-all-tokens') ->expectsOutput('All XERO Tokens in storage') ->assertExitCode(0); }); test('command displays tokens in table format', function () { // Create a token $token = XeroToken::factory()->create([ 'tenant_name' => 'Test Tenant', 'tenant_id' => 'test-tenant-id', ]); // Run the command $this->artisan('xero:show-all-tokens') ->expectsOutput('All XERO Tokens in storage') ->assertExitCode(0); // Verify the token exists in the database $this->assertDatabaseHas('xero_tokens', [ 'tenant_name' => 'Test Tenant', 'tenant_id' => 'test-tenant-id', ]); }); test('command handles encrypted tokens correctly', function () { // Enable encryption Config::set('xero.encrypt', true); // Create a token with encrypted values $token = XeroToken::factory()->create([ 'tenant_name' => 'Encrypted Tenant', 'tenant_id' => 'encrypted-tenant-id', 'access_token' => Crypt::encryptString('encrypted-access-token'), 'refresh_token' => Crypt::encryptString('encrypted-refresh-token'), ]); // Run the command $this->artisan('xero:show-all-tokens') ->expectsOutput('All XERO Tokens in storage') ->assertExitCode(0); // Verify the token exists in the database with encrypted values $this->assertDatabaseHas('xero_tokens', [ 'tenant_name' => 'Encrypted Tenant', 'tenant_id' => 'encrypted-tenant-id', ]); // Reset config Config::set('xero.encrypt', false); }); test('command handles decryption exceptions', function () { // Enable encryption Config::set('xero.encrypt', true); // Create a token with non-encrypted values that will cause decryption to fail $token = XeroToken::factory()->create([ 'tenant_name' => 'Exception Tenant', 'tenant_id' => 'exception-tenant-id', 'access_token' => 'non-encrypted-access-token', 'refresh_token' => 'non-encrypted-refresh-token', ]); // Run the command $this->artisan('xero:show-all-tokens') ->expectsOutput('All XERO Tokens in storage') ->assertExitCode(0); // Verify the token exists in the database $this->assertDatabaseHas('xero_tokens', [ 'tenant_name' => 'Exception Tenant', 'tenant_id' => 'exception-tenant-id', 'access_token' => 'non-encrypted-access-token', 'refresh_token' => 'non-encrypted-refresh-token', ]); // Reset config Config::set('xero.encrypt', false); }); PKºqW]¯7¼ó‡ ‡ @laravel-xero/tests/Console/Commands/XeroKeepAliveCommandTest.phpnu„[µü¤app->singleton('command.xero.keep-alive', fn () => new XeroKeepAliveCommand()); Artisan::registerCommand($this->app->make('command.xero.keep-alive')); }); test('command handles no tokens gracefully', function () { // Ensure no tokens exist XeroToken::query()->delete(); // Run the command $this->artisan('xero:keep-alive') ->assertExitCode(0); }); test('command refreshes token for connected tenant', function () { // Create a token $token = XeroToken::factory()->create([ 'tenant_name' => 'Connected Tenant', 'tenant_id' => 'connected-tenant-id', ]); // Mock the Xero facade Xero::shouldReceive('setTenantId') ->once() ->with('connected-tenant-id'); Xero::shouldReceive('isConnected') ->once() ->andReturn(true); Xero::shouldReceive('getAccessToken') ->once() ->with(false) ->andReturn('refreshed-token'); // Run the command $this->artisan('xero:keep-alive') ->expectsOutput('Refreshing Token for Tenant: Connected Tenant - Successful') ->assertExitCode(0); }); test('command handles not connected tenant', function () { // Create a token $token = XeroToken::factory()->create([ 'tenant_name' => 'Not Connected Tenant', 'tenant_id' => 'not-connected-tenant-id', ]); // Mock the Xero facade Xero::shouldReceive('setTenantId') ->once() ->with('not-connected-tenant-id'); Xero::shouldReceive('isConnected') ->once() ->andReturn(false); // Run the command $this->artisan('xero:keep-alive') ->expectsOutput('Refreshing Token for Tenant: Not Connected Tenant - Not Connected') ->assertExitCode(0); }); test('command handles multiple tenants', function () { // Create multiple tokens $token1 = XeroToken::factory()->create([ 'tenant_name' => 'Tenant 1', 'tenant_id' => 'tenant-id-1', ]); $token2 = XeroToken::factory()->create([ 'tenant_name' => 'Tenant 2', 'tenant_id' => 'tenant-id-2', ]); // Mock the Xero facade for first tenant (connected) Xero::shouldReceive('setTenantId') ->once() ->with('tenant-id-1'); Xero::shouldReceive('isConnected') ->once() ->andReturn(true); Xero::shouldReceive('getAccessToken') ->once() ->with(false) ->andReturn('refreshed-token-1'); // Mock the Xero facade for second tenant (not connected) Xero::shouldReceive('setTenantId') ->once() ->with('tenant-id-2'); Xero::shouldReceive('isConnected') ->once() ->andReturn(false); // Run the command $this->artisan('xero:keep-alive') ->expectsOutput('Refreshing Token for Tenant: Tenant 1 - Successful') ->expectsOutput('Refreshing Token for Tenant: Tenant 2 - Not Connected') ->assertExitCode(0); }); PKºqW]}o�‘OOlaravel-xero/tests/XeroTest.phpnu„[µü¤XeroMock = Mockery::mock(Xero::class); }); test('can initialise', function () { $this->assertInstanceOf(Xero::class, $this->XeroMock); }); test('redirected when connect is called', function () { $connect = XeroFacade::connect(); $this->assertInstanceOf(RedirectResponse::class, $connect); }); test('is connected returns false when no data in db', function () { $connect = XeroFacade::isConnected(); expect($connect)->toBeFalse(); }); test('is connected returns true when data exists in db', function () { XeroToken::create([ 'id' => 0, 'access_token' => '1234', 'expires_in' => strtotime('+1 day'), 'scopes' => 'contacts', ]); $connect = XeroFacade::isConnected(); expect($connect)->toBeTrue(); }); test('disconnect returns true when data exists in db', function () { Http::fake(); XeroToken::create([ 'id' => 0, 'access_token' => '1234', 'expires_in' => strtotime('+1 day'), 'scopes' => 'contacts', ]); XeroFacade::disconnect(); $this->assertDatabaseCount('xero_tokens', 0); }); test('getTokenData returns XeroToken data', function () { XeroToken::create([ 'id' => 0, 'access_token' => '1234', 'expires_in' => strtotime('+1 day'), 'scopes' => 'contacts', ]); $data = XeroFacade::getTokenData(); expect($data)->toBeObject(); }); test('getTokenData when no tokens exist returns null', function () { expect(XeroFacade::getTokenData())->toBeNull; }); test('getTenantId returns id', function () { XeroToken::create([ 'id' => 0, 'access_token' => '1234', 'expires_in' => strtotime('+1 day'), 'scopes' => 'contacts', 'tenant_id' => '1234', ]); expect(XeroFacade::getTenantId())->toBe('1234'); }); test('getTenantName returns name', function () { XeroToken::create([ 'id' => 0, 'access_token' => '1234', 'expires_in' => strtotime('+1 day'), 'scopes' => 'contacts', 'tenant_id' => '1234', 'tenant_name' => 'Jones', ]); expect(XeroFacade::getTenantName())->toBe('Jones'); }); test('can return getAccessToken when it has not expired ', function () { Http::fake(); XeroToken::create([ 'id' => 0, 'access_token' => '1234', 'expires_in' => now()->addMinutes(25), 'updated_at' => strtotime('+1 day'), 'scopes' => 'contacts', ]); $data = XeroFacade::getAccessToken(); expect($data)->toBe('1234'); }); test('can get tokens when not-encrypted but encryption is enabled', function () { Config::set('xero.encrypt', true); XeroToken::create([ 'id' => 0, 'access_token' => '1234', 'expires_in' => strtotime('+1 day'), 'scopes' => 'contacts', ]); $data = XeroFacade::getAccessToken(); expect($data)->toBe('1234'); }); test('can get tokens when encrypted', function () { Config::set('xero.encrypt', true); XeroToken::create([ 'id' => 0, 'access_token' => Crypt::encryptString('1234'), 'expires_in' => strtotime('+1 day'), 'scopes' => 'contacts', ]); $data = XeroFacade::getAccessToken(); expect($data)->toBe('1234'); }); test('formats Microsoft JSON date with timezone offset', function () { $input = '/Date(1663257600000+0100)/'; $formatted = Xero::formatDate($input); expect($formatted)->toBe('2022-09-15 16:00:00'); }); test('formats standard ISO 8601 date string', function () { $input = '2023-05-19T14:00:00+00:00'; $formatted = Xero::formatDate($input); expect($formatted)->toBe('2023-05-19 14:00:00'); }); test('returns empty string for invalid date input', function () { $input = 'invalid-date-format'; $formatted = Xero::formatDate($input); expect($formatted)->toBe(''); }); test('formats Microsoft JSON date with UTC offset', function () { $input = '/Date(1663257600000+0000)/'; $formatted = Xero::formatDate($input); expect($formatted)->toBe('2022-09-15 16:00:00'); }); PKºqW]Ý,ƒù��,laravel-xero/tests/XeroAuthenticatedTest.phpnu„[µü¤once()->andReturn(false); Xero::shouldReceive('connect')->once()->andReturn(new RedirectResponse('xero-connect-url')); // Create middleware instance $middleware = new XeroAuthenticated(); // Create a request $request = Request::create('/test', 'GET'); // Execute middleware $response = $middleware->handle($request, function () { return 'next middleware'; }); // Assert that we get a redirect response expect($response)->toBeInstanceOf(RedirectResponse::class) ->and($response->getTargetUrl())->toBe('xero-connect-url'); }); test('middleware continues when connected', function () { // Mock the Xero facade Xero::shouldReceive('isConnected')->once()->andReturn(true); // Create middleware instance $middleware = new XeroAuthenticated(); // Create a request $request = Request::create('/test', 'GET'); // Execute middleware $response = $middleware->handle($request, function () { return 'next middleware'; }); // Assert that we continue to the next middleware expect($response)->toBe('next middleware'); }); PKºqW]PäSîÒÒ0laravel-xero/tests/Resources/CreditNotesTest.phpnu„[µü¤filter('bogus', 1) ->get(); })->throws(InvalidArgumentException::class, "Filter option 'bogus' is not valid."); test('filter returns object', function () { $filter = (new CreditNotes)->filter('ids', '1234'); expect($filter)->toBeObject(); }); PKºqW]'3ÊÊ-laravel-xero/tests/Resources/WebhooksTest.phpnu„[µü¤webhooks = new Webhooks(); }); // Helper function to mock file_get_contents function mockFileGetContents($payload) { // Create a stream wrapper to mock php://input stream_wrapper_unregister('php'); stream_wrapper_register('php', MockPhpStream::class); file_put_contents('php://input', $payload); } // Mock stream wrapper class class MockPhpStream { protected static $data = ''; public function stream_open($path, $mode, $options, &$opened_path) { return true; } public function stream_read($count) { $ret = mb_substr(self::$data, 0, $count); self::$data = mb_substr(self::$data, $count); return $ret; } public function stream_write($data) { self::$data = $data; return mb_strlen($data); } public function stream_tell() { return 0; } public function stream_eof() { return true; } public function stream_stat() { return []; } } afterEach(function () { // Restore the original stream wrapper if (in_array('php', stream_get_wrappers())) { stream_wrapper_unregister('php'); stream_wrapper_restore('php'); } }); test('getSignature returns correct signature', function () { // Mock the payload $payload = json_encode(['events' => [['eventType' => 'test']]]); // Use reflection to set protected property $reflection = new ReflectionClass($this->webhooks); $property = $reflection->getProperty('payload'); $property->setAccessible(true); $property->setValue($this->webhooks, $payload); // Set webhook key in config Config::set('xero.webhookKey', 'test-webhook-key'); // Calculate expected signature $expectedSignature = base64_encode(hash_hmac('sha256', $payload, 'test-webhook-key', true)); // Test the method expect($this->webhooks->getSignature())->toBe($expectedSignature); }); test('validate returns true when signatures match', function () { // Mock the payload and server variables $payload = json_encode(['events' => [['eventType' => 'test']]]); // Mock file_get_contents to return our payload mockFileGetContents($payload); // Set webhook key in config Config::set('xero.webhookKey', 'test-webhook-key'); // Calculate signature $signature = base64_encode(hash_hmac('sha256', $payload, 'test-webhook-key', true)); // Mock $_SERVER $_SERVER['HTTP_X_XERO_SIGNATURE'] = $signature; // Test the method expect($this->webhooks->validate())->toBeTrue(); }); test('validate returns false when signatures do not match', function () { // Mock the payload and server variables $payload = json_encode(['events' => [['eventType' => 'test']]]); // Mock file_get_contents to return our payload mockFileGetContents($payload); // Set webhook key in config Config::set('xero.webhookKey', 'test-webhook-key'); // Set incorrect signature $_SERVER['HTTP_X_XERO_SIGNATURE'] = 'incorrect-signature'; // Test the method expect($this->webhooks->validate())->toBeFalse(); }); test('getEvents returns events from payload', function () { // Create a mock of Webhooks with validate method always returning true $webhooks = Mockery::mock(Webhooks::class)->makePartial(); $webhooks->shouldReceive('validate')->andReturn(true); // Mock payload with events $events = [ (object) ['eventType' => 'test1', 'resourceId' => '1'], (object) ['eventType' => 'test2', 'resourceId' => '2'], ]; $payload = json_encode(['events' => $events]); // Use reflection to set protected property $reflection = new ReflectionClass($webhooks); $property = $reflection->getProperty('payload'); $property->setAccessible(true); $property->setValue($webhooks, $payload); // Test the method $result = $webhooks->getEvents(); // Verify each event has the expected properties expect($result[0]->eventType)->toBe('test1'); expect($result[0]->resourceId)->toBe('1'); expect($result[1]->eventType)->toBe('test2'); expect($result[1]->resourceId)->toBe('2'); }); PKºqW]+gä2ÉÉ-laravel-xero/tests/Resources/ContactsTest.phpnu„[µü¤filter('bogus', 1) ->get(); })->throws(InvalidArgumentException::class, "Filter option 'bogus' is not valid."); test('filter returns object', function () { $filter = (new Contacts)->filter('ids', '1234'); expect($filter)->toBeObject(); }); PKºqW]|‰ÖóÉÉ-laravel-xero/tests/Resources/InvoicesTest.phpnu„[µü¤filter('bogus', 1) ->get(); })->throws(InvalidArgumentException::class, "Filter option 'bogus' is not valid."); test('filter returns object', function () { $filter = (new Invoices)->filter('ids', '1234'); expect($filter)->toBeObject(); }); PKºqW]�rÎgTTlaravel-xero/pint.jsonnu„[µü¤{ "preset": "laravel", "rules": { "array_push": true, "backtick_to_shell_exec": true, "date_time_immutable": true, "declare_strict_types": true, "lowercase_keywords": true, "lowercase_static_reference": true, "final_class": false, "final_internal_class": false, "final_public_method_for_abstract_class": false, "fully_qualified_strict_types": true, "global_namespace_import": { "import_classes": true, "import_constants": true, "import_functions": true }, "mb_str_functions": true, "modernize_types_casting": true, "new_with_parentheses": false, "no_superfluous_elseif": true, "no_useless_else": true, "no_multiple_statements_per_line": true, "ordered_class_elements": { "order": [ "use_trait", "case", "constant", "constant_public", "constant_protected", "constant_private", "property_public", "property_protected", "property_private", "construct", "destruct", "magic", "phpunit", "method_abstract", "method_public_static", "method_public", "method_protected_static", "method_protected", "method_private_static", "method_private" ], "sort_algorithm": "none" }, "ordered_interfaces": true, "ordered_traits": true, "protected_to_private": true, "self_accessor": true, "self_static_accessor": true, "strict_comparison": true, "visibility_required": true } } PKºqW] öþÊÊlaravel-xero/phpunit.xmlnu„[µü¤ src/ ./tests ./src PKºqW]HŠC‰**laravel-xero/phpstan.neonnu„[µü¤includes: - vendor/larastan/larastan/extension.neon parameters: paths: - src/ # Level 9 is the highest level level: 5 # ignoreErrors: # - '#PHPDoc tag @var#' # # excludePaths: # - ./*/*/FileToBeExcluded.php # # checkMissingIterableValueType: falsePKºqW]¾ ¦5§§!laravel-xero/src/Facades/Xero.phpnu„[µü¤value, // ACCREC for sales invoices, ACCPAY for bills public ?string $invoiceNumber = null, public ?string $reference = null, public ?string $date = null, public ?string $dueDate = null, public ?string $status = InvoiceStatus::Draft->value, public ?string $lineAmountTypes = InvoiceLineAmountType::Exclusive->value, public ?string $currencyCode = null, public ?string $currencyRate = null, public ?string $subTotal = null, public ?string $totalTax = null, public ?string $total = null, public ?string $contactID = null, /** @var array> */ public ?array $contact = null, /** @var array> */ public ?array $lineItems = [], /** @var array> */ public ?array $payments = [], /** @var array> */ public ?array $creditNotes = [], /** @var array> */ public ?array $prepayments = [], /** @var array> */ public ?array $overpayments = [], public ?bool $hasAttachments = false, public ?bool $isDiscounted = false, public ?bool $hasErrors = false, ) {} /** * Create a line item array for the invoice * * @param array>|null $tracking * @return array */ public static function createLineItem( ?string $description = null, string|int|null $quantity = null, string|float|null $unitAmount = null, ?int $accountCode = null, ?string $itemCode = null, ?string $taxType = null, ?string $taxAmount = null, ?string $lineAmount = null, ?string $discountRate = null, ?array $tracking = null, ): array { return array_filter([ 'Description' => $description, 'Quantity' => $quantity, 'UnitAmount' => $unitAmount, 'AccountCode' => $accountCode, 'ItemCode' => $itemCode, 'TaxType' => $taxType, 'TaxAmount' => $taxAmount, 'LineAmount' => $lineAmount, 'DiscountRate' => $discountRate, 'Tracking' => $tracking, ], function (mixed $value) { return $value !== null; }); } /** * Convert the DTO to an array for the Xero API * * @return array */ public function toArray(): array { return array_filter([ 'InvoiceID' => $this->invoiceID, 'Type' => $this->type, 'InvoiceNumber' => $this->invoiceNumber, 'Reference' => $this->reference, 'Date' => $this->date, 'DueDate' => $this->dueDate, 'Status' => $this->status, 'LineAmountTypes' => $this->lineAmountTypes, 'CurrencyCode' => $this->currencyCode, 'CurrencyRate' => $this->currencyRate, 'SubTotal' => $this->subTotal, 'TotalTax' => $this->totalTax, 'Total' => $this->total, 'Contact' => $this->contactID ? ['ContactID' => $this->contactID] : $this->contact, 'LineItems' => $this->lineItems, 'HasAttachments' => $this->hasAttachments, 'IsDiscounted' => $this->isDiscounted, ], fn (mixed $value) => $value !== null && $value !== []); } } PKºqW]¡…Ÿ8””$laravel-xero/src/DTOs/ContactDTO.phpnu„[µü¤value, public ?bool $isSupplier = false, public ?bool $isCustomer = false, public ?string $defaultCurrency = null, public ?string $website = null, public ?string $purchasesDefaultAccountCode = null, public ?string $salesDefaultAccountCode = null, /** @var array> */ public ?array $addresses = [], /** @var array> */ public ?array $phones = [], /** @var array> */ public ?array $contactPersons = [], public ?bool $hasAttachments = false, public ?bool $hasValidationErrors = false, ) {} /** * Create an address array for the contact * * @return array */ public static function createAddress( string $addressType, ?string $addressLine1 = null, ?string $addressLine2 = null, ?string $addressLine3 = null, ?string $addressLine4 = null, ?string $city = null, ?string $region = null, ?string $postalCode = null, ?string $country = null, ?string $attentionTo = null ): array { return [ 'AddressType' => $addressType, 'AddressLine1' => $addressLine1, 'AddressLine2' => $addressLine2, 'AddressLine3' => $addressLine3, 'AddressLine4' => $addressLine4, 'City' => $city, 'Region' => $region, 'PostalCode' => $postalCode, 'Country' => $country, 'AttentionTo' => $attentionTo, ]; } /** * Create a phone array for the contact * * @return array */ public static function createPhone( string $phoneType, ?string $phoneNumber = null, ?string $phoneAreaCode = null, ?string $phoneCountryCode = null ): array { return [ 'PhoneType' => $phoneType, 'PhoneNumber' => $phoneNumber, 'PhoneAreaCode' => $phoneAreaCode, 'PhoneCountryCode' => $phoneCountryCode, ]; } /** * Create a contact person array for the contact * * @return array */ public static function createContactPerson( ?string $firstName = null, ?string $lastName = null, ?string $emailAddress = null, ?bool $includeInEmails = false ): array { return [ 'FirstName' => $firstName, 'LastName' => $lastName, 'EmailAddress' => $emailAddress, 'IncludeInEmails' => $includeInEmails, ]; } /** * Convert the DTO to an array for the Xero API * * @return array */ public function toArray(): array { return array_filter([ 'Name' => $this->name, 'FirstName' => $this->firstName, 'LastName' => $this->lastName, 'EmailAddress' => $this->emailAddress, 'AccountNumber' => $this->accountNumber, 'BankAccountDetails' => $this->bankAccountDetails, 'TaxNumber' => $this->taxNumber, 'AccountsReceivableTaxType' => $this->accountsReceivableTaxType, 'AccountsPayableTaxType' => $this->accountsPayableTaxType, 'ContactStatus' => $this->contactStatus, 'IsSupplier' => $this->isSupplier, 'IsCustomer' => $this->isCustomer, 'DefaultCurrency' => $this->defaultCurrency, 'Website' => $this->website, 'PurchasesDefaultAccountCode' => $this->purchasesDefaultAccountCode, 'SalesDefaultAccountCode' => $this->salesDefaultAccountCode, 'Addresses' => $this->addresses, 'Phones' => $this->phones, 'ContactPersons' => $this->contactPersons, 'HasAttachments' => $this->hasAttachments, 'HasValidationErrors' => $this->hasValidationErrors, ], function (mixed $value) { return $value !== null; }); } } PKºqW]¤GÆÛÛ(laravel-xero/src/Enums/InvoiceStatus.phpnu„[µü¤ $case->value, self::cases()); return in_array($value, $validValues); } } PKºqW]¸;­��0laravel-xero/src/Enums/InvoiceLineAmountType.phpnu„[µü¤ $case->value, self::cases()); return in_array($value, $validValues); } } PKºqW]ZjpS__&laravel-xero/src/Enums/InvoiceType.phpnu„[µü¤ $case->value, self::cases()); return in_array($value, $validValues); } } PKºqW]Úá�‹‹(laravel-xero/src/Enums/ContactStatus.phpnu„[µü¤ $case->value, self::cases()); return in_array($value, $validValues); } } PKºqW]²Ã##(laravel-xero/src/Enums/FilterOptions.phpnu„[µü¤ $case->value, self::cases()); return in_array($value, $validValues); } } PKºqW]¯2ÈÉVV%laravel-xero/src/Models/XeroToken.phpnu„[µü¤ 'integer', ]; protected static function newFactory(): TokenFactory { return TokenFactory::new(); } /** * @return Attribute */ protected function expires(): Attribute { return Attribute::get( fn (): DateTimeInterface => $this->updated_at->addSeconds((int) $this->expires_in) ); } } PKºqW] çnb††claravel-xero/src/database/migrations/2024_03_15_135030_increase_xero_token_tables_string_length.phpnu„[µü¤text('refresh_token')->change(); }); } /** * Reverse the migrations. */ public function down(): void { Schema::table('xero_tokens', function (Blueprint $table) { $table->string('refresh_token')->change(); }); } }; PKºqW]o;'ØÆÆAlaravel-xero/src/database/migrations/create_xero_tokens_table.phpnu„[µü¤increments('id'); $table->text('id_token')->nullable(); $table->text('access_token'); $table->string('expires_in')->nullable(); $table->string('token_type')->nullable(); $table->string('refresh_token')->nullable(); $table->string('scopes'); $table->string('auth_event_id')->nullable(); $table->string('tenant_id')->nullable(); $table->string('tenant_type')->nullable(); $table->string('tenant_name')->nullable(); $table->string('created_date_utc')->nullable(); $table->string('updated_date_utc')->nullable(); $table->timestamps(); }); } /** * Reverse the migrations. */ public function down(): void { Schema::dropIfExists('xero_tokens'); } } PKºqW]^JÙzàà4laravel-xero/src/database/factories/TokenFactory.phpnu„[µü¤ $this->faker->uuid, 'tenant_name' => $this->faker->name, 'access_token' => $this->faker->uuid, 'refresh_token' => $this->faker->uuid, 'expires_in' => $this->faker->randomNumber(), 'created_at' => $this->faker->dateTime, 'updated_at' => $this->faker->dateTime, 'scopes' => config('xero.scopes'), ]; } } PKºqW]cÖ×î��,laravel-xero/src/Traits/XeroHelpersTrait.phpnu„[µü¤setTimezone(new DateTimeZone($tzOffset)); return $dt->format($format); } // Fallback to default DateTime parsing $dt = new DateTimeImmutable($date); return $dt->format($format); } catch (Throwable $e) { // Invalid date input, return empty string instead of crashing return ''; } } /** * Format query strings for API requests. */ public static function formatQueryStrings(array $params): string { return app(formatQueryStringsAction::class)($params); } }PKºqW]j„jíí5laravel-xero/src/Actions/formatQueryStringsAction.phpnu„[µü¤delete(); if ($this->isRunningInConsole()) { throw new Exception('Xero token has expired, please re-authenticate.'); } return redirect()->away(config('xero.redirectUri')); } return null; } /** * @throws Exception */ protected function isRunningInConsole(): bool { return app()->runningInConsole(); } } PKºqW]£k­y¼¼-laravel-xero/src/Actions/StoreTokenAction.phpnu„[µü¤ $token['id_token'], 'access_token' => config('xero.encrypt') ? Crypt::encryptString($token['access_token']) : $token['access_token'], 'expires_in' => $token['expires_in'], 'token_type' => $token['token_type'], 'refresh_token' => config('xero.encrypt') ? Crypt::encryptString($token['refresh_token']) : $token['refresh_token'], 'scopes' => $token['scope'], ]; if ($tenantId) { if ($tenantData !== []) { $data = array_merge($data, $tenantData); } $where = ['tenant_id' => $tenantId]; } elseif ($tenantData !== []) { $data = array_merge($data, $tenantData); $where = ['tenant_id' => $data['tenant_id']]; } else { $where = ['id' => 1]; } return XeroToken::updateOrCreate($where, $data); } } PKºqW]? >9//(laravel-xero/src/XeroServiceProvider.phpnu„[µü¤loadMigrationsFrom(__DIR__.'/../database/migrations'); $this->registerCommands(); $this->registerMiddleware($router); $this->configurePublishing(); } public function registerCommands(): void { if (! $this->app->runningInConsole()) { return; } $this->commands([ XeroKeepAliveCommand::class, XeroShowAllTokensCommand::class ]); } public function registerMiddleware(Router $router): void { // add middleware $router->aliasMiddleware('XeroAuthenticated', XeroAuthenticated::class); } public function configurePublishing(): void { if (! $this->app->runningInConsole()) { return; } // Publishing the configuration file. $this->publishes([ __DIR__.'/../config/xero.php' => config_path('xero.php'), ], 'config'); $timestamp = date('Y_m_d_His', time()); $this->publishes([ __DIR__.'/database/migrations/create_xero_tokens_table.php' => $this->app->databasePath()."/migrations/{$timestamp}_create_xero_tokens_table.php", ], 'migrations'); } /** * Register any package services. */ public function register(): void { $this->mergeConfigFrom(__DIR__.'/../config/xero.php', 'xero'); // Register the service the package provides. $this->app->singleton('xero', function () { return new Xero; }); } /** * Get the services provided by the provider. */ public function provides(): array { return ['xero']; } } PKºqW]ÔÅ¢wMM>laravel-xero/src/Console/Commands/XeroShowAllTokensCommand.phpnu„[µü¤newLine(); $this->line('All XERO Tokens in storage'); $this->newLine(); $dataToDisplay = [ 'id', 'tenant_name', 'tenant_id', 'updated_at', ]; // Fetch all access tokens $tokens = XeroToken::select($dataToDisplay)->get(); if (config('xero.encrypt')) { $tokens->map(function (XeroToken $token) { try { $access_token = Crypt::decryptString($token->access_token); } catch (DecryptException $e) { $access_token = $token->access_token; } // Split them as a refresh token may not exist... try { $refresh_token = Crypt::decryptString($token->refresh_token); } catch (DecryptException $e) { $refresh_token = $token->refresh_token; } $token->access_token = $access_token; $token->refresh_token = $refresh_token; return $token; }); } $this->table( $dataToDisplay, $tokens->toArray() ); } } PKºqW]n�žLL:laravel-xero/src/Console/Commands/XeroKeepAliveCommand.phpnu„[µü¤newLine(); // Fetch all tenants for when multiple tenants are in use. $tenants = XeroToken::all(); foreach ($tenants as $tenant) { // Set the tenant ID Xero::setTenantId($tenant->tenant_id); if (Xero::isConnected()) { Xero::getAccessToken($redirectWhenNotConnected = false); $this->info('Refreshing Token for Tenant: '.$tenant->tenant_name.' - Successful'); } else { $this->info('Refreshing Token for Tenant: '.$tenant->tenant_name.' - Not Connected'); } } } } PKºqW] ×õî®)®)laravel-xero/src/Xero.phpnu„[µü¤guzzle($function, $path, $data, $raw, $accept, $headers); } // request verb is not in the $options array throw new RuntimeException($function.' is not a valid HTTP Verb'); } public function setTenantId(string $tenant_id): void { $this->tenant_id = $tenant_id; } public function contacts(): Contacts { return new Contacts; } public function creditnotes(): CreditNotes { return new CreditNotes; } public function invoices(): Invoices { return new Invoices; } public function webhooks(): Webhooks { return new Webhooks; } public function isTokenValid(): bool { $token = $this->getTokenData(); if ($token === null) { return false; } $now = now()->addMinutes(5); if ($token->expires < $now) { return false; } return true; } public function isConnected(): bool { return ! ($this->getTokenData() === null); } public function disconnect(): void { try { $token = $this->getTokenData(); Http::withHeaders([ 'authorization' => 'Basic '.base64_encode(config('xero.clientId').':'.config('xero.clientSecret')), ]) ->asForm() ->post(self::$revokeUrl, [ 'token' => $token->refresh_token, ])->throw(); $token->delete(); } catch (Exception $e) { throw new RuntimeException('error getting tenant: '.$e->getMessage()); } } /** * Make a connection or return a token where it's valid * * * @throws Exception */ public function connect(): RedirectResponse|Application|Redirector { // when no code param redirect to Microsoft if (request()->has('code')) { // With the authorization code, we can retrieve access tokens and other data. try { $params = [ 'grant_type' => 'authorization_code', 'code' => request('code'), 'redirect_uri' => config('xero.redirectUri'), ]; $result = $this->sendPost(self::$tokenUrl, $params); try { $response = Http::withHeaders([ 'Authorization' => 'Bearer '.$result['access_token'], ]) ->acceptJson() ->get(self::$connectionUrl) ->throw() ->json(); foreach ($response as $tenant) { $tenantData = [ 'auth_event_id' => $tenant['authEventId'], 'tenant_id' => $tenant['tenantId'], 'tenant_type' => $tenant['tenantType'], 'tenant_name' => $tenant['tenantName'], 'created_date_utc' => $tenant['createdDateUtc'], 'updated_date_utc' => $tenant['updatedDateUtc'], ]; app(StoreTokenAction::class)($result, $tenantData, $tenant['tenantId']); } } catch (Exception $e) { throw new Exception('Error getting tenant: '.$e->getMessage()); } return redirect(config('xero.landingUri')); } catch (Exception $e) { throw new Exception($e->getMessage()); } } $url = self::$authorizeUrl.'?'.http_build_query([ 'response_type' => 'code', 'client_id' => config('xero.clientId'), 'redirect_uri' => config('xero.redirectUri'), 'scope' => config('xero.scopes'), ]); return redirect()->away($url); } public function getTokenData(): ?XeroToken { if ($this->tenant_id) { $token = XeroToken::where('tenant_id', '=', $this->tenant_id)->first(); } else { $token = XeroToken::first(); } if ($token && config('xero.encrypt')) { try { $access_token = Crypt::decryptString($token->access_token); } catch (DecryptException $e) { $access_token = $token->access_token; } // Split them as a refresh token may not exist... try { $refresh_token = Crypt::decryptString($token->refresh_token); } catch (DecryptException $e) { $refresh_token = $token->refresh_token; } $token->access_token = $access_token; $token->refresh_token = $refresh_token; } return $token; } /** * @throws Exception */ public function getAccessToken(bool $redirectWhenNotConnected = true): string { /* @var XeroToken $token */ $token = $this->getTokenData(); $this->redirectIfNoToken($token, $redirectWhenNotConnected); $now = now()->addMinutes(5); if ($token->expires < $now) { return $this->renewExpiringToken($token); } return $token->access_token; } /** * @throws Exception */ public function renewExpiringToken(XeroToken $token): string { $params = [ 'grant_type' => 'refresh_token', 'refresh_token' => $token->refresh_token, 'redirect_uri' => config('xero.redirectUri'), ]; $result = $this->sendPost(self::$tokenUrl, $params); app(tokenExpiredAction::class)($result, $token); app(StoreTokenAction::class)($result, ['tenant_id' => $token->tenant_id], $this->tenant_id); return $result['access_token']; } public function getTenantId(): string { $token = $this->getTokenData(); $this->redirectIfNoToken($token); return $token->tenant_id; } public function getTenantName(): ?string { // use id if passed otherwise use logged-in user $token = $this->getTokenData(); $this->redirectIfNoToken($token); // Token is still valid, just return it return $token->tenant_name; } /** * @throws Exception */ protected static function sendPost(string $url, array $params): array { try { $response = Http::withHeaders([ 'authorization' => 'Basic '.base64_encode(config('xero.clientId').':'.config('xero.clientSecret')), ]) ->asForm() ->acceptJson() ->post($url, $params); return $response->json(); } catch (Exception $e) { throw new Exception($e->getMessage()); } } protected function redirectIfNoToken(XeroToken $token, bool $redirectWhenNotConnected = true): RedirectResponse|bool { // Check if tokens exist otherwise run the oauth request if (! $this->isConnected() && $redirectWhenNotConnected === true) { return redirect()->away(config('xero.redirectUri')); } return false; } /** * run Guzzle to process the requested url * * @throws Exception */ protected function guzzle(string $type, string $request, array $data = [], bool $raw = false, string $accept = 'application/json', array $headers = []): array { if ($data === []) { $data = null; } try { $response = Http::withToken($this->getAccessToken()) ->withHeaders(array_merge(['Xero-tenant-id' => $this->getTenantId()], $headers)) ->accept($accept) ->$type(self::$baseUrl.$request, $data) ->throw(); return [ 'body' => $raw ? $response->body() : $response->json(), 'headers' => $response->getHeaders(), ]; } catch (RequestException $e) { $response = json_decode($e->response->body()); throw new Exception($response->Detail ?? "Type: $response?->Type Message: $response?->Message Error Number: $response?->ErrorNumber"); } catch (Exception $e) { throw new Exception($e->getMessage()); } } } PKºqW]w{ª±*laravel-xero/src/Resources/CreditNotes.phpnu„[µü¤queryString[$key] = $value; return $this; } public function get(): array { $queryString = $this->formatQueryStrings($this->queryString); $result = parent::get('CreditNotes?'.$queryString); return $result['body']['CreditNotes']; } public function find(string $contactId): array { $result = parent::get('CreditNotes/'.$contactId); return $result['body']['CreditNotes'][0]; } public function update(string $contactId, array $data): array { $result = $this->post('CreditNotes/'.$contactId, $data); return $result['body']['CreditNotes'][0]; } public function store(array $data): array { $result = $this->post('CreditNotes', $data); return $result['body']['CreditNotes'][0]; } } PKºqW]ƒ¡•èè'laravel-xero/src/Resources/Contacts.phpnu„[µü¤queryString[$key] = $value; return $this; } public function get(): array { $queryString = $this->formatQueryStrings($this->queryString); $result = parent::get('Contacts?'.$queryString); return $result['body']['Contacts']; } public function find(string $contactId): array { $result = parent::get('Contacts/'.$contactId); return $result['body']['Contacts'][0]; } public function update(string $contactId, array $data): array { $result = $this->post('Contacts/'.$contactId, $data); return $result['body']['Contacts'][0]; } public function store(array $data): array { $result = $this->post('Contacts', $data); return $result['body']['Contacts'][0]; } } PKºqW]ëï'laravel-xero/src/Resources/Invoices.phpnu„[µü¤queryString[$key] = $value; return $this; } public function get(): array { $queryString = $this->formatQueryStrings($this->queryString); $result = parent::get('Invoices?'.$queryString); return $result['body']['Invoices']; } public function find(string $invoiceId): array { $result = parent::get('Invoices/'.$invoiceId); return $result['body']['Invoices'][0]; } public function onlineUrl(string $invoiceId): string { $result = parent::get('Invoices/'.$invoiceId.'/OnlineInvoice'); return $result['body']['OnlineInvoices'][0]['OnlineInvoiceUrl']; } public function update(string $invoiceId, array $data): array { $result = parent::post('Invoices/'.$invoiceId, $data); return $result['body']['Invoices'][0]; } public function store(array $data): array { $result = parent::post('Invoices', $data); return $result['body']['Invoices'][0]; } public function attachments(string $invoiceId): array { $result = parent::get('Invoices/'.$invoiceId.'/Attachments'); return $result['body']['Attachments']; } public function attachment(string $invoiceId, ?string $attachmentId = null, ?string $fileName = null): string { // Depending on the application, we may want to get it by the FileName instead of the AttachmentId $nameOrId = $attachmentId ? $attachmentId : $fileName; $result = parent::get('Invoices/'.$invoiceId.'/Attachments/'.$nameOrId); return $result['body']; } } PKºqW]±‚¨sÂÂ'laravel-xero/src/Resources/Webhooks.phpnu„[µü¤payload = file_get_contents('php://input'); $signature = $_SERVER['HTTP_X_XERO_SIGNATURE']; return hash_equals($this->getSignature(), $signature); } public function getSignature(): string { return base64_encode(hash_hmac('sha256', $this->payload, config('xero.webhookKey'), true)); } public function getEvents(): array { $this->validate(); $payload = json_decode($this->payload); return $payload->events; } } PKºqW]ôÂÏÏlaravel-xero/composer.jsonnu„[µü¤{ "name": "dcblogdev/laravel-xero", "description": "A Laravel Xero package", "license": "MIT", "authors": [ { "name": "David Carr", "email": "dave@dcblog.dev", "homepage": "https://dcblog.dev" } ], "homepage": "https://github.com/dcblogdev/laravel-xero", "keywords": ["Laravel", "Xero"], "require": { "illuminate/support": "5.5.x|5.6.x|5.7.x|5.8.x|6.x|7.x|8.x|9.x|10.x|11.x|12.x", "league/oauth2-client": "^1.4|^2.8.1", "guzzlehttp/guzzle": "6.x|^7.9.3", "ext-json": "*", "ext-fileinfo": "*", "ext-curl": "*" }, "require-dev": { "orchestra/testbench": "^7.0|^10.3", "pestphp/pest": "^1.21|^v3.8.2", "pestphp/pest-plugin-laravel": "^v3.2.0", "pestphp/pest-plugin-type-coverage": "^3.5.1", "larastan/larastan": "^3.4", "mockery/mockery": "^1.6.12", "laravel/pint": "^1.22.1" }, "autoload": { "psr-4": { "Dcblogdev\\Xero\\": "src/", "Dcblogdev\\Xero\\database\\factories\\": "database/factories/", "Dcblogdev\\Xero\\Tests\\": "tests" } }, "autoload-dev": { "classmap": [ "tests/TestCase.php" ] }, "scripts": { "pint": "vendor/bin/pint", "stan": "phpstan analyse", "pest": "pest", "pest-type-coverage": "pest --type-coverage", "pest-coverage": "pest --parallel --coverage", "check": [ "@pint", "@stan", "@pest-type-coverage", "@pest-coverage" ] }, "extra": { "laravel": { "providers": [ "Dcblogdev\\Xero\\XeroServiceProvider" ], "aliases": { "Xero": "Dcblogdev\\Xero\\Facades\\Xero" } } }, "config": { "allow-plugins": { "pestphp/pest-plugin": true } } } PKºqW] h¦VVlaravel-xero/.gitignorenu„[µü¤.DS_Store .idea .vscode vendor composer.lock .phpunit.result.cache .php-cs-fixer.cachePKºqW] }¶:pplaravel-xero/changelog.mdnu„[µü¤# Changelog All notable changes to `Laravel Xero` will be documented in this file. ## Version 1.0.0 - Everything ## Version 1.0.1 - changed id_token to be text ## Version 1.0.2 - all records by default and added raw option ## Version 1.0.3 - Add support for multiple tenant storing. ## Version 1.1.0 - only expect tenant id when tenantData is being used ## Version 1.1.1 - Add new command keep-alive ## Version 1.1.2 - Added command `xero:show-all` to show all tokens stored in the database. ## Version 1.1.3 - Update keep-alive for multi-tenant use, and storeToken where id when using tenant_id in the class ## Version 1.1.4 - Added support for Laravel 10 ## Version 1.1.5 - applied fix for returning access token ## Version 1.1.6 - Update Xero.php expiry check to use the accessor instead of expired_in column - Fixed failing test for getting access tokenPKºqW]—ë¬6­­laravel-xero/config/xero.phpnu„[µü¤ env('XERO_CLIENT_ID'), /* * set the client secret */ 'clientSecret' => env('XERO_CLIENT_SECRET'), /* * Set the url to trigger the oauth process */ 'redirectUri' => env('XERO_REDIRECT_URL'), /* * Set the url to redirecto once authenticated; */ 'landingUri' => env('XERO_LANDING_URL', '/'), /** * Set access token, when set will bypass the oauth2 process */ 'accessToken' => env('XERO_ACCESS_TOKEN', ''), /** * Set webhook token */ 'webhookKey' => env('XERO_WEBHOOK_KEY', ''), /** * Set the scopes */ 'scopes' => env('XERO_SCOPES', 'openid email profile offline_access accounting.settings accounting.transactions accounting.contacts'), /** * Encrypt tokens in database? */ 'encrypt' => env('XERO_ENCRYPT', false), ]; PKºqW]|ÇCC laravel-xero/.github/FUNDING.ymlnu„[µü¤# These are supported funding model platforms github: [dcblogdev] PKºqW]¹ÕÕJ��&laravel-xero/.github/workflows/php.ymlnu„[µü¤name: PHP Pipeline on: [push, pull_request] jobs: build: runs-on: ubuntu-latest strategy: max-parallel: 2 matrix: php-versions: ['8.3', '8.4'] name: PHP ${{ matrix.php-versions }} steps: - uses: actions/checkout@v1 - name: Setup PHP uses: shivammathur/setup-php@master with: php-version: ${{ matrix.php-versions }} coverage: xdebug - name: Validate composer.json and composer.lock run: composer validate - name: Install dependencies run: composer install --prefer-dist --no-progress --no-suggest - name: Run test suite run: ./vendor/bin/pest PKºqW]Ïz·CClaravel-xero/license.mdnu„[µü¤# The license The MIT License (MIT) Copyright (c) 2020 dcblogdev Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. PKºqW]Ÿ„¾¥==laravel-xero/readme.mdnu„[µü¤PKºqW]Â:4N¼¼*ƒlaravel-xero/tests/DTOs/InvoiceDTOTest.phpnu„[µü¤PKºqW]µè¸c*™(laravel-xero/tests/DTOs/ContactDTOTest.phpnu„[µü¤PKºqW]ü°U¡¡,Flaravel-xero/tests/Enums/InvoiceTypeTest.phpnu„[µü¤PKºqW]2cx®®.Hlaravel-xero/tests/Enums/FilterOptionsTest.phpnu„[µü¤PKºqW]Ȱgm6Klaravel-xero/tests/Enums/InvoiceLineAmountTypeTest.phpnu„[µü¤PKºqW]³'¥vv.~Mlaravel-xero/tests/Enums/InvoiceStatusTest.phpnu„[µü¤PKºqW]>�rsââ.RPlaravel-xero/tests/Enums/ContactStatusTest.phpnu„[µü¤PKºqW]–Æ~ñ  +’Rlaravel-xero/tests/Models/XeroTokenTest.phpnu„[µü¤PKºqW]Í<*íí\laravel-xero/tests/TestCase.phpnu„[µü¤PKºqW]l.ëâ â 3=`laravel-xero/tests/Actions/StoreTokenActionTest.phpnu„[µü¤PKºqW]Ë]¼‹‹;‚llaravel-xero/tests/Actions/formatQueryStringsActionTest.phpnu„[µü¤PKºqW]–:b‰  ;xolaravel-xero/tests/Actions/HandleTokenExpiredActionTest.phpnu„[µü¤PKºqW]èìè8iiwlaravel-xero/tests/Pest.phpnu„[µü¤PKºqW]žü&ï� � D·wlaravel-xero/tests/Console/Commands/XeroShowAllTokensCommandTest.phpnu„[µü¤PKºqW]¯7¼ó‡ ‡ @º„laravel-xero/tests/Console/Commands/XeroKeepAliveCommandTest.phpnu„[µü¤PKºqW]}o�‘OO±‘laravel-xero/tests/XeroTest.phpnu„[µü¤PKºqW]Ý,ƒù��,O£laravel-xero/tests/XeroAuthenticatedTest.phpnu„[µü¤PKºqW]PäSîÒÒ0H©laravel-xero/tests/Resources/CreditNotesTest.phpnu„[µü¤PKºqW]'3ÊÊ-z«laravel-xero/tests/Resources/WebhooksTest.phpnu„[µü¤PKºqW]+gä2ÉÉ-¡¼laravel-xero/tests/Resources/ContactsTest.phpnu„[µü¤PKºqW]|‰ÖóÉÉ-Ǿlaravel-xero/tests/Resources/InvoicesTest.phpnu„[µü¤PKºqW]�rÎgTTíÀlaravel-xero/pint.jsonnu„[µü¤PKºqW] öþÊʇÈlaravel-xero/phpunit.xmlnu„[µü¤PKºqW]HŠC‰**™Ìlaravel-xero/phpstan.neonnu„[µü¤PKºqW]¾ ¦5§§! Îlaravel-xero/src/Facades/Xero.phpnu„[µü¤PKºqW]¿ç†ŠÐÐ&Ñlaravel-xero/src/XeroAuthenticated.phpnu„[µü¤PKºqW]Ä\@'  $*Ólaravel-xero/src/DTOs/InvoiceDTO.phpnu„[µü¤PKºqW]¡…Ÿ8””$‰âlaravel-xero/src/DTOs/ContactDTO.phpnu„[µü¤PKºqW]¤GÆÛÛ(qõlaravel-xero/src/Enums/InvoiceStatus.phpnu„[µü¤PKºqW]¸;­��0¤÷laravel-xero/src/Enums/InvoiceLineAmountType.phpnu„[µü¤PKºqW]ZjpS__&“ùlaravel-xero/src/Enums/InvoiceType.phpnu„[µü¤PKºqW]Úá�‹‹(Hûlaravel-xero/src/Enums/ContactStatus.phpnu„[µü¤PKºqW]²Ã##(+ýlaravel-xero/src/Enums/FilterOptions.phpnu„[µü¤PKºqW]¯2ÈÉVV%¦ÿlaravel-xero/src/Models/XeroToken.phpnu„[µü¤PKºqW] çnb††cQlaravel-xero/src/database/migrations/2024_03_15_135030_increase_xero_token_tables_string_length.phpnu„[µü¤PKºqW]o;'ØÆÆAjlaravel-xero/src/database/migrations/create_xero_tokens_table.phpnu„[µü¤PKºqW]^JÙzàà4¡ laravel-xero/src/database/factories/TokenFactory.phpnu„[µü¤PKºqW]cÖ×î��,ålaravel-xero/src/Traits/XeroHelpersTrait.phpnu„[µü¤PKºqW]j„jíí5Âlaravel-xero/src/Actions/formatQueryStringsAction.phpnu„[µü¤PKºqW]gG|à/laravel-xero/src/Actions/tokenExpiredAction.phpnu„[µü¤PKºqW]£k­y¼¼-„laravel-xero/src/Actions/StoreTokenAction.phpnu„[µü¤PKºqW]? >9//(�laravel-xero/src/XeroServiceProvider.phpnu„[µü¤PKºqW]ÔÅ¢wMM>$(laravel-xero/src/Console/Commands/XeroShowAllTokensCommand.phpnu„[µü¤PKºqW]n�žLL:ß/laravel-xero/src/Console/Commands/XeroKeepAliveCommand.phpnu„[µü¤PKºqW] ×õî®)®)•4laravel-xero/src/Xero.phpnu„[µü¤PKºqW]w{ª±*Œ^laravel-xero/src/Resources/CreditNotes.phpnu„[µü¤PKºqW]ƒ¡•èè'ëclaravel-xero/src/Resources/Contacts.phpnu„[µü¤PKºqW]ëï'*ilaravel-xero/src/Resources/Invoices.phpnu„[µü¤PKºqW]±‚¨sÂÂ'œqlaravel-xero/src/Resources/Webhooks.phpnu„[µü¤PKºqW]ôÂÏϵtlaravel-xero/composer.jsonnu„[µü¤PKºqW] h¦VVÎ|laravel-xero/.gitignorenu„[µü¤PKºqW] }¶:ppk}laravel-xero/changelog.mdnu„[µü¤PKºqW]—ë¬6­­$�laravel-xero/config/xero.phpnu„[µü¤PKºqW]|ÇCC …laravel-xero/.github/FUNDING.ymlnu„[µü¤PKºqW]¹ÕÕJ��&°…laravel-xero/.github/workflows/php.ymlnu„[µü¤PKºqW]Ïz·CC–ˆlaravel-xero/license.mdnu„[µü¤PK99£ �