mirror of
https://github.com/invoiceninja/invoiceninja.git
synced 2026-03-03 03:07:01 +00:00
Merge pull request #11608 from turbo124/v5-develop
Bug Fix - Webhook creation erroneously
This commit is contained in:
@@ -65,7 +65,7 @@ class RecurringInvoiceItemExport extends BaseExport
|
|||||||
if (count($this->input['report_keys']) == 0) {
|
if (count($this->input['report_keys']) == 0) {
|
||||||
$this->force_keys = true;
|
$this->force_keys = true;
|
||||||
$this->input['report_keys'] = array_values($this->mergeItemsKeys('recurring_invoice_report_keys'));
|
$this->input['report_keys'] = array_values($this->mergeItemsKeys('recurring_invoice_report_keys'));
|
||||||
nlog($this->input['report_keys']);
|
// nlog($this->input['report_keys']);
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->input['report_keys'] = array_merge($this->input['report_keys'], array_diff($this->forced_client_fields, $this->input['report_keys']));
|
$this->input['report_keys'] = array_merge($this->input['report_keys'], array_diff($this->forced_client_fields, $this->input['report_keys']));
|
||||||
|
|||||||
@@ -20,15 +20,23 @@ use App\Services\Quickbooks\QuickbooksService;
|
|||||||
|
|
||||||
class ImportQuickbooksController extends BaseController
|
class ImportQuickbooksController extends BaseController
|
||||||
{
|
{
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine if the user is authorized to make this request.
|
* authorizeQuickbooks
|
||||||
*
|
*
|
||||||
|
* Starts the Quickbooks authorization process.
|
||||||
|
*
|
||||||
|
* @param mixed $request
|
||||||
|
* @param string $token
|
||||||
|
* @return RedirectResponse
|
||||||
*/
|
*/
|
||||||
public function authorizeQuickbooks(AuthQuickbooksRequest $request, string $token)
|
public function authorizeQuickbooks(AuthQuickbooksRequest $request, string $token)
|
||||||
{
|
{
|
||||||
|
|
||||||
MultiDB::findAndSetDbByCompanyKey($request->getTokenContent()['company_key']);
|
MultiDB::findAndSetDbByCompanyKey($request->getTokenContent()['company_key']);
|
||||||
|
|
||||||
$company = $request->getCompany();
|
$company = $request->getCompany();
|
||||||
|
|
||||||
$qb = new QuickbooksService($company);
|
$qb = new QuickbooksService($company);
|
||||||
|
|
||||||
$authorizationUrl = $qb->sdk()->getAuthorizationUrl();
|
$authorizationUrl = $qb->sdk()->getAuthorizationUrl();
|
||||||
@@ -39,18 +47,45 @@ class ImportQuickbooksController extends BaseController
|
|||||||
|
|
||||||
return redirect()->to($authorizationUrl);
|
return redirect()->to($authorizationUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* onAuthorized
|
||||||
|
*
|
||||||
|
* Handles the callback from Quickbooks after authorization.
|
||||||
|
*
|
||||||
|
* @param AuthorizedQuickbooksRequest $request
|
||||||
|
* @return RedirectResponse
|
||||||
|
*/
|
||||||
public function onAuthorized(AuthorizedQuickbooksRequest $request)
|
public function onAuthorized(AuthorizedQuickbooksRequest $request)
|
||||||
{
|
{
|
||||||
|
|
||||||
|
nlog($request->all());
|
||||||
|
|
||||||
MultiDB::findAndSetDbByCompanyKey($request->getTokenContent()['company_key']);
|
MultiDB::findAndSetDbByCompanyKey($request->getTokenContent()['company_key']);
|
||||||
$company = $request->getCompany();
|
$company = $request->getCompany();
|
||||||
|
|
||||||
$qb = new QuickbooksService($company);
|
$qb = new QuickbooksService($company);
|
||||||
|
|
||||||
$realm = $request->query('realmId');
|
$realm = $request->query('realmId');
|
||||||
|
|
||||||
|
nlog($realm);
|
||||||
|
|
||||||
$access_token_object = $qb->sdk()->accessTokenFromCode($request->query('code'), $realm);
|
$access_token_object = $qb->sdk()->accessTokenFromCode($request->query('code'), $realm);
|
||||||
|
|
||||||
|
nlog($access_token_object);
|
||||||
|
|
||||||
$qb->sdk()->saveOAuthToken($access_token_object);
|
$qb->sdk()->saveOAuthToken($access_token_object);
|
||||||
|
|
||||||
|
// Refresh the service to initialize SDK with the new access token
|
||||||
|
$qb->refresh();
|
||||||
|
|
||||||
|
$companyInfo = $qb->sdk()->company();
|
||||||
|
|
||||||
|
$company->quickbooks->companyName = $companyInfo->CompanyName;
|
||||||
|
$company->save();
|
||||||
|
|
||||||
|
nlog($companyInfo);
|
||||||
|
|
||||||
return redirect(config('ninja.react_url'));
|
return redirect(config('ninja.react_url'));
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,9 +14,10 @@ namespace App\Http\Controllers;
|
|||||||
|
|
||||||
use App\Libraries\MultiDB;
|
use App\Libraries\MultiDB;
|
||||||
use Illuminate\Http\Response;
|
use Illuminate\Http\Response;
|
||||||
|
use App\Services\Quickbooks\QuickbooksService;
|
||||||
|
use App\Http\Requests\Quickbooks\SyncQuickbooksRequest;
|
||||||
use App\Http\Requests\Quickbooks\ConfigQuickbooksRequest;
|
use App\Http\Requests\Quickbooks\ConfigQuickbooksRequest;
|
||||||
use App\Http\Requests\Quickbooks\DisconnectQuickbooksRequest;
|
use App\Http\Requests\Quickbooks\DisconnectQuickbooksRequest;
|
||||||
use App\Http\Requests\Quickbooks\SyncQuickbooksRequest;
|
|
||||||
|
|
||||||
class QuickbooksController extends BaseController
|
class QuickbooksController extends BaseController
|
||||||
{
|
{
|
||||||
@@ -53,7 +54,9 @@ class QuickbooksController extends BaseController
|
|||||||
$company = $user->company();
|
$company = $user->company();
|
||||||
|
|
||||||
$qb = new QuickbooksService($company);
|
$qb = new QuickbooksService($company);
|
||||||
$qb->sdk()->revokeAccessToken();
|
$rs = $qb->sdk()->revokeAccessToken();
|
||||||
|
|
||||||
|
nlog($rs);
|
||||||
|
|
||||||
$company->quickbooks = null;
|
$company->quickbooks = null;
|
||||||
$company->save();
|
$company->save();
|
||||||
|
|||||||
@@ -303,7 +303,6 @@ class WebhookController extends BaseController
|
|||||||
|
|
||||||
$webhook = WebhookFactory::create($user->company()->id, $user->id);
|
$webhook = WebhookFactory::create($user->company()->id, $user->id);
|
||||||
$webhook->fill($request->all());
|
$webhook->fill($request->all());
|
||||||
$webhook->save();
|
|
||||||
|
|
||||||
return $this->itemResponse($webhook);
|
return $this->itemResponse($webhook);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ class PreviewInvoiceRequest extends Request
|
|||||||
};
|
};
|
||||||
|
|
||||||
if ($invitation) {
|
if ($invitation) {
|
||||||
nlog($invitation->toArray());
|
// nlog($invitation->toArray());
|
||||||
return $invitation;
|
return $invitation;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,11 +32,15 @@ class ClientTransformer extends BaseTransformer
|
|||||||
throw new ImportException('Client already exists');
|
throw new ImportException('Client already exists');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$address1 = data_get($data, 'Street', data_get($data, 'Address Line 1', ''));
|
||||||
|
$address2 = data_get($data, 'Address Line 2', '');
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'company_id' => $this->company->id,
|
'company_id' => $this->company->id,
|
||||||
'name' => $this->getString($data, 'Organization'),
|
'name' => $this->getString($data, 'Organization'),
|
||||||
'phone' => $this->getString($data, 'Phone'),
|
'phone' => $this->getString($data, 'Phone'),
|
||||||
'address1' => $this->getString($data, 'Street'),
|
'address1' => $address1,
|
||||||
|
'address2' => $address2,
|
||||||
'city' => $this->getString($data, 'City'),
|
'city' => $this->getString($data, 'City'),
|
||||||
'state' => $this->getString($data, 'Province/State'),
|
'state' => $this->getString($data, 'Province/State'),
|
||||||
'postal_code' => $this->getString($data, 'Postal Code'),
|
'postal_code' => $this->getString($data, 'Postal Code'),
|
||||||
|
|||||||
@@ -119,13 +119,19 @@ class EmailMailable extends Mailable
|
|||||||
|
|
||||||
$file = $document->getFile();
|
$file = $document->getFile();
|
||||||
|
|
||||||
|
if (empty($file)) {
|
||||||
|
nlog("EmailMailable: Document file is empty: {$document->url}");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
$finfo = finfo_open(FILEINFO_MIME_TYPE);
|
$finfo = finfo_open(FILEINFO_MIME_TYPE);
|
||||||
$mime = finfo_buffer($finfo, $file);
|
$mime = finfo_buffer($finfo, $file);
|
||||||
$mime = $mime ?: 'application/octet-stream';
|
$mime = $mime ?: 'application/octet-stream';
|
||||||
finfo_close($finfo);
|
finfo_close($finfo);
|
||||||
|
|
||||||
return Attachment::fromData(fn () => $file, $document->name)->withMime($mime);
|
return Attachment::fromData(fn () => $file, $document->name)->withMime($mime);
|
||||||
});
|
})
|
||||||
|
->filter();
|
||||||
|
|
||||||
return $attachments->merge($documents)->toArray();
|
return $attachments->merge($documents)->toArray();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ class QuickbooksService
|
|||||||
'ClientSecret' => config('services.quickbooks.client_secret'),
|
'ClientSecret' => config('services.quickbooks.client_secret'),
|
||||||
'auth_mode' => 'oauth2',
|
'auth_mode' => 'oauth2',
|
||||||
'scope' => "com.intuit.quickbooks.accounting",
|
'scope' => "com.intuit.quickbooks.accounting",
|
||||||
'RedirectURI' => $this->testMode ? 'https://grok.romulus.com.au/quickbooks/authorized' : 'https://invoicing.co/quickbooks/authorized',
|
'RedirectURI' => $this->testMode ? 'https://qb.romulus.com.au/quickbooks/authorized' : 'https://invoicing.co/quickbooks/authorized',
|
||||||
'baseUrl' => $this->testMode ? CoreConstants::SANDBOX_DEVELOPMENT : CoreConstants::QBO_BASEURL,
|
'baseUrl' => $this->testMode ? CoreConstants::SANDBOX_DEVELOPMENT : CoreConstants::QBO_BASEURL,
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -134,6 +134,24 @@ class QuickbooksService
|
|||||||
// return $this;
|
// return $this;
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refresh the service after OAuth token has been updated.
|
||||||
|
* This reloads the company from the database and reinitializes the SDK
|
||||||
|
* with the new access token.
|
||||||
|
*
|
||||||
|
* @return self
|
||||||
|
*/
|
||||||
|
public function refresh(): self
|
||||||
|
{
|
||||||
|
// Reload company from database to get fresh token data
|
||||||
|
$this->company = $this->company->fresh();
|
||||||
|
|
||||||
|
// Reinitialize the SDK with the updated token
|
||||||
|
$this->init();
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
private function checkToken(): self
|
private function checkToken(): self
|
||||||
{
|
{
|
||||||
|
|
||||||
@@ -201,6 +219,52 @@ class QuickbooksService
|
|||||||
return isset($this->settings->{$entity}->direction) && ($this->settings->{$entity}->direction === $direction || $this->settings->{$entity}->direction === \App\Enum\SyncDirection::BIDIRECTIONAL);
|
return isset($this->settings->{$entity}->direction) && ($this->settings->{$entity}->direction === $direction || $this->settings->{$entity}->direction === \App\Enum\SyncDirection::BIDIRECTIONAL);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// [
|
||||||
|
// QuickBooksOnline\API\Data\IPPAccount {#7706
|
||||||
|
// +Id: "30",
|
||||||
|
// +SyncToken: "0",
|
||||||
|
// +MetaData: QuickBooksOnline\API\Data\IPPModificationMetaData {#7707
|
||||||
|
// +CreatedByRef: null,
|
||||||
|
// +CreateTime: "2024-05-22T14:46:30-07:00",
|
||||||
|
// +LastModifiedByRef: null,
|
||||||
|
// +LastUpdatedTime: "2024-05-22T14:46:30-07:00",
|
||||||
|
// +LastChangedInQB: null,
|
||||||
|
// +Synchronized: null,
|
||||||
|
// },
|
||||||
|
// +CustomField: null,
|
||||||
|
// +AttachableRef: null,
|
||||||
|
// +domain: null,
|
||||||
|
// +status: null,
|
||||||
|
// +sparse: null,
|
||||||
|
// +Name: "Uncategorized Income",
|
||||||
|
// +SubAccount: "false",
|
||||||
|
// +ParentRef: null,
|
||||||
|
// +Description: null,
|
||||||
|
// +FullyQualifiedName: "Uncategorized Income",
|
||||||
|
// +AccountAlias: null,
|
||||||
|
// +TxnLocationType: null,
|
||||||
|
// +Active: "true",
|
||||||
|
// +Classification: "Revenue",
|
||||||
|
// +AccountType: "Income",
|
||||||
|
// +AccountSubType: "ServiceFeeIncome",
|
||||||
|
// +AccountPurposes: null,
|
||||||
|
// +AcctNum: null,
|
||||||
|
// +AcctNumExtn: null,
|
||||||
|
// +BankNum: null,
|
||||||
|
// +OpeningBalance: null,
|
||||||
|
// +OpeningBalanceDate: null,
|
||||||
|
// +CurrentBalance: "0",
|
||||||
|
// +CurrentBalanceWithSubAccounts: "0",
|
||||||
|
// +CurrencyRef: "USD",
|
||||||
|
// +TaxAccount: null,
|
||||||
|
// +TaxCodeRef: null,
|
||||||
|
// +OnlineBankingEnabled: null,
|
||||||
|
// +FIName: null,
|
||||||
|
// +JournalCodeRef: null,
|
||||||
|
// +AccountEx: null,
|
||||||
|
// },
|
||||||
|
// ]
|
||||||
/**
|
/**
|
||||||
* Fetch income accounts from QuickBooks.
|
* Fetch income accounts from QuickBooks.
|
||||||
*
|
*
|
||||||
@@ -223,6 +287,52 @@ class QuickbooksService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// [
|
||||||
|
// QuickBooksOnline\API\Data\IPPAccount {#7709
|
||||||
|
// +Id: "57",
|
||||||
|
// +SyncToken: "0",
|
||||||
|
// +MetaData: QuickBooksOnline\API\Data\IPPModificationMetaData {#7698
|
||||||
|
// +CreatedByRef: null,
|
||||||
|
// +CreateTime: "2024-05-27T10:17:24-07:00",
|
||||||
|
// +LastModifiedByRef: null,
|
||||||
|
// +LastUpdatedTime: "2024-05-27T10:17:24-07:00",
|
||||||
|
// +LastChangedInQB: null,
|
||||||
|
// +Synchronized: null,
|
||||||
|
// },
|
||||||
|
// +CustomField: null,
|
||||||
|
// +AttachableRef: null,
|
||||||
|
// +domain: null,
|
||||||
|
// +status: null,
|
||||||
|
// +sparse: null,
|
||||||
|
// +Name: "Workers Compensation",
|
||||||
|
// +SubAccount: "true",
|
||||||
|
// +ParentRef: "11",
|
||||||
|
// +Description: null,
|
||||||
|
// +FullyQualifiedName: "Insurance:Workers Compensation",
|
||||||
|
// +AccountAlias: null,
|
||||||
|
// +TxnLocationType: null,
|
||||||
|
// +Active: "true",
|
||||||
|
// +Classification: "Expense",
|
||||||
|
// +AccountType: "Expense",
|
||||||
|
// +AccountSubType: "Insurance",
|
||||||
|
// +AccountPurposes: null,
|
||||||
|
// +AcctNum: null,
|
||||||
|
// +AcctNumExtn: null,
|
||||||
|
// +BankNum: null,
|
||||||
|
// +OpeningBalance: null,
|
||||||
|
// +OpeningBalanceDate: null,
|
||||||
|
// +CurrentBalance: "0",
|
||||||
|
// +CurrentBalanceWithSubAccounts: "0",
|
||||||
|
// +CurrencyRef: "USD",
|
||||||
|
// +TaxAccount: null,
|
||||||
|
// +TaxCodeRef: null,
|
||||||
|
// +OnlineBankingEnabled: null,
|
||||||
|
// +FIName: null,
|
||||||
|
// +JournalCodeRef: null,
|
||||||
|
// +AccountEx: null,
|
||||||
|
// },
|
||||||
|
// ]
|
||||||
/**
|
/**
|
||||||
* Fetch expense accounts from QuickBooks.
|
* Fetch expense accounts from QuickBooks.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -55,6 +55,11 @@ class SdkWrapper
|
|||||||
return $this->accessToken()->getRefreshToken();
|
return $this->accessToken()->getRefreshToken();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function revokeAccessToken()
|
||||||
|
{
|
||||||
|
return $this->sdk->getOAuth2LoginHelper()->revokeToken($this->accessToken()->getAccessToken());
|
||||||
|
}
|
||||||
|
|
||||||
public function company()
|
public function company()
|
||||||
{
|
{
|
||||||
return $this->sdk->getCompanyInfo();
|
return $this->sdk->getCompanyInfo();
|
||||||
|
|||||||
@@ -50,6 +50,21 @@ class BaseTransformer
|
|||||||
return $currency ? (string) $currency->id : $this->company->settings->currency_id;
|
return $currency ? (string) $currency->id : $this->company->settings->currency_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function resolveTimezone(?string $timezone_name): string
|
||||||
|
{
|
||||||
|
if (empty($timezone_name)) {
|
||||||
|
return (string) $this->company->settings->timezone_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @var \App\Models\Timezone $timezone */
|
||||||
|
$timezone = app('timezones')->first(function ($t) use ($timezone_name) {
|
||||||
|
/** @var \App\Models\Timezone $t */
|
||||||
|
return $t->name === $timezone_name;
|
||||||
|
});
|
||||||
|
|
||||||
|
return $timezone ? (string) $timezone->id : (string) $this->company->settings->timezone_id;
|
||||||
|
}
|
||||||
|
|
||||||
public function getShipAddrCountry($data, $field)
|
public function getShipAddrCountry($data, $field)
|
||||||
{
|
{
|
||||||
return is_null(($c = $this->getString($data, $field))) ? null : $this->getCountryId($c);
|
return is_null(($c = $this->getString($data, $field))) ? null : $this->getCountryId($c);
|
||||||
|
|||||||
99
app/Services/Quickbooks/Transformers/CompanyTransformer.php
Normal file
99
app/Services/Quickbooks/Transformers/CompanyTransformer.php
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invoice Ninja (https://invoiceninja.com).
|
||||||
|
*
|
||||||
|
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2025. Invoice Ninja LLC (https://invoiceninja.com)
|
||||||
|
*
|
||||||
|
* @license https://www.elastic.co/licensing/elastic-license
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Services\Quickbooks\Transformers;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transforms QuickBooks IPPCompanyInfo into Invoice Ninja company data.
|
||||||
|
*
|
||||||
|
* QB fields: CompanyName, LegalName, CompanyAddr, LegalAddr, CustomerCommunicationAddr,
|
||||||
|
* Email, CustomerCommunicationEmailAddr, PrimaryPhone, WebAddr, CompanyURL,
|
||||||
|
* Country, DefaultTimeZone.
|
||||||
|
*/
|
||||||
|
class CompanyTransformer extends BaseTransformer
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Transform QuickBooks company info to Ninja structure.
|
||||||
|
*
|
||||||
|
* @param mixed $qb_data QuickBooksOnline\API\Data\IPPCompanyInfo (or array)
|
||||||
|
* @return array{quickbooks: array, settings: array}
|
||||||
|
*/
|
||||||
|
public function qbToNinja(mixed $qb_data): array
|
||||||
|
{
|
||||||
|
return $this->transform($qb_data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function ninjaToQb(): void
|
||||||
|
{
|
||||||
|
// Reserved for Ninja → QB sync when needed.
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param mixed $data IPPCompanyInfo object or array
|
||||||
|
* @return array{quickbooks: array<string, mixed>, settings: array<string, mixed>}
|
||||||
|
*/
|
||||||
|
public function transform(mixed $data): array
|
||||||
|
{
|
||||||
|
$addr = $this->pickAddress($data);
|
||||||
|
$country_raw = data_get($addr, 'Country') ?? data_get($addr, 'CountryCode') ?? data_get($data, 'Country');
|
||||||
|
$country_id = $this->resolveCountry($country_raw);
|
||||||
|
|
||||||
|
$quickbooks = [
|
||||||
|
'companyName' => data_get($data, 'CompanyName', '') ?: data_get($data, 'LegalName', ''),
|
||||||
|
];
|
||||||
|
|
||||||
|
$settings = [
|
||||||
|
'address1' => data_get($addr, 'Line1', ''),
|
||||||
|
'address2' => data_get($addr, 'Line2', ''),
|
||||||
|
'city' => data_get($addr, 'City', ''),
|
||||||
|
'state' => data_get($addr, 'CountrySubDivisionCode', ''),
|
||||||
|
'postal_code' => data_get($addr, 'PostalCode', ''),
|
||||||
|
'country_id' => $country_id,
|
||||||
|
'phone' => $this->pickPhone($data),
|
||||||
|
'email' => $this->pickEmail($data),
|
||||||
|
'website' => data_get($data, 'WebAddr', '') ?: data_get($data, 'CompanyURL', ''),
|
||||||
|
'timezone_id' => $this->resolveTimezone(data_get($data, 'DefaultTimeZone')),
|
||||||
|
];
|
||||||
|
|
||||||
|
return [
|
||||||
|
'quickbooks' => $quickbooks,
|
||||||
|
'settings' => $settings,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prefer CompanyAddr, then LegalAddr, then CustomerCommunicationAddr.
|
||||||
|
*
|
||||||
|
* @param mixed $data
|
||||||
|
* @return object|array|null
|
||||||
|
*/
|
||||||
|
private function pickAddress(mixed $data)
|
||||||
|
{
|
||||||
|
$addr = data_get($data, 'CompanyAddr') ?? data_get($data, 'LegalAddr') ?? data_get($data, 'CustomerCommunicationAddr');
|
||||||
|
|
||||||
|
return is_object($addr) ? $addr : (is_array($addr) ? $addr : []);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function pickPhone(mixed $data): string
|
||||||
|
{
|
||||||
|
$phone = data_get($data, 'PrimaryPhone.FreeFormNumber');
|
||||||
|
|
||||||
|
return is_string($phone) ? $phone : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function pickEmail(mixed $data): string
|
||||||
|
{
|
||||||
|
$email = data_get($data, 'Email.Address') ?? data_get($data, 'CustomerCommunicationEmailAddr.Address') ?? data_get($data, 'CompanyEmailAddr');
|
||||||
|
|
||||||
|
return is_string($email) ? $email : '';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -828,6 +828,11 @@ class TemplateService
|
|||||||
|
|
||||||
foreach ($refund['invoices'] as $refunded_invoice) {
|
foreach ($refund['invoices'] as $refunded_invoice) {
|
||||||
$invoice = Invoice::withTrashed()->find($refunded_invoice['invoice_id']);
|
$invoice = Invoice::withTrashed()->find($refunded_invoice['invoice_id']);
|
||||||
|
|
||||||
|
if (!$invoice) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
$amount = Number::formatMoney($refunded_invoice['amount'], $payment->client);
|
$amount = Number::formatMoney($refunded_invoice['amount'], $payment->client);
|
||||||
$notes = ctrans('texts.status_partially_refunded_amount', ['amount' => $amount]);
|
$notes = ctrans('texts.status_partially_refunded_amount', ['amount' => $amount]);
|
||||||
|
|
||||||
|
|||||||
@@ -194,6 +194,8 @@ class HtmlEngine
|
|||||||
$data['$payment_schedule'] = ['value' => '', 'label' => ctrans('texts.payment_schedule')];
|
$data['$payment_schedule'] = ['value' => '', 'label' => ctrans('texts.payment_schedule')];
|
||||||
$data['$payment_schedule_interval'] = ['value' => '', 'label' => ctrans('texts.payment_schedule')];
|
$data['$payment_schedule_interval'] = ['value' => '', 'label' => ctrans('texts.payment_schedule')];
|
||||||
|
|
||||||
|
$data['$days_overdue'] = ['value' => $this->daysOverdue(), 'label' => ctrans('texts.overdue')];
|
||||||
|
|
||||||
if(method_exists($this->entity, 'paymentSchedule')) {
|
if(method_exists($this->entity, 'paymentSchedule')) {
|
||||||
$data['$payment_schedule'] = ['value' => $this->entity->paymentSchedule(true), 'label' => ctrans('texts.payment_schedule')];
|
$data['$payment_schedule'] = ['value' => $this->entity->paymentSchedule(true), 'label' => ctrans('texts.payment_schedule')];
|
||||||
$data['$payment_schedule_interval'] = ['value' => $this->entity->paymentScheduleInterval(), 'label' => ctrans('texts.payment_schedule')];
|
$data['$payment_schedule_interval'] = ['value' => $this->entity->paymentScheduleInterval(), 'label' => ctrans('texts.payment_schedule')];
|
||||||
@@ -692,6 +694,7 @@ class HtmlEngine
|
|||||||
$data['$task.rate'] = ['value' => '', 'label' => ctrans('texts.rate')];
|
$data['$task.rate'] = ['value' => '', 'label' => ctrans('texts.rate')];
|
||||||
$data['$task.cost'] = ['value' => '', 'label' => ctrans('texts.rate')];
|
$data['$task.cost'] = ['value' => '', 'label' => ctrans('texts.rate')];
|
||||||
$data['$task.hours'] = ['value' => '', 'label' => ctrans('texts.hours')];
|
$data['$task.hours'] = ['value' => '', 'label' => ctrans('texts.hours')];
|
||||||
|
$data['$task.total_hours'] = ['value' => $this->totalTaskHours(), 'label' => ctrans('texts.total_hours')];
|
||||||
$data['$task.tax'] = ['value' => '', 'label' => ctrans('texts.tax')];
|
$data['$task.tax'] = ['value' => '', 'label' => ctrans('texts.tax')];
|
||||||
$data['$task.tax_name1'] = ['value' => '', 'label' => ctrans('texts.tax')];
|
$data['$task.tax_name1'] = ['value' => '', 'label' => ctrans('texts.tax')];
|
||||||
$data['$task.tax_name2'] = ['value' => '', 'label' => ctrans('texts.tax')];
|
$data['$task.tax_name2'] = ['value' => '', 'label' => ctrans('texts.tax')];
|
||||||
@@ -899,6 +902,48 @@ Código seguro de verificación (CSV): {$verifactu_log->status}";
|
|||||||
return "<tr><td>{$text}</td></tr><tr><td><img src=\"data:image/png;base64,{$qr_code}\" alt=\"Verifactu QR Code\"></td></tr>";
|
return "<tr><td>{$text}</td></tr><tr><td><img src=\"data:image/png;base64,{$qr_code}\" alt=\"Verifactu QR Code\"></td></tr>";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* totalTaskHours
|
||||||
|
*
|
||||||
|
* calculates the total hours of all tasks in the invoice
|
||||||
|
*
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
private function totalTaskHours()
|
||||||
|
{
|
||||||
|
return collect($this->entity->line_items)
|
||||||
|
->filter(function ($item) {
|
||||||
|
return $item->type_id == '2';
|
||||||
|
})
|
||||||
|
->sum('quantity');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* daysOverdue
|
||||||
|
*
|
||||||
|
* calculates the number of days overdue the entity is
|
||||||
|
*
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
private function daysOverdue()
|
||||||
|
{
|
||||||
|
if($this->entity->partial > 0 && !empty($this->entity->partial_due_date)) {
|
||||||
|
|
||||||
|
$days_overdue = \Carbon\Carbon::parse($this->entity->partial_due_date)->diffInDays(now()->startOfDay()->setTimezone($this->entity->company->timezone()->name));
|
||||||
|
|
||||||
|
return max($days_overdue, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!empty($this->entity->due_date)) {
|
||||||
|
|
||||||
|
$days_overdue = \Carbon\Carbon::parse($this->entity->due_date)->diffInDays(now()->startOfDay()->setTimezone($this->entity->company->timezone()->name));
|
||||||
|
|
||||||
|
return max($days_overdue, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
private function getPaymentMeta(\App\Models\Payment $payment)
|
private function getPaymentMeta(\App\Models\Payment $payment)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -12,19 +12,20 @@
|
|||||||
|
|
||||||
namespace App\Utils;
|
namespace App\Utils;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
use App\Utils\Ninja;
|
||||||
use App\Models\Account;
|
use App\Models\Account;
|
||||||
use App\Models\Country;
|
use App\Models\Country;
|
||||||
use App\Models\CreditInvitation;
|
|
||||||
use App\Models\InvoiceInvitation;
|
|
||||||
use App\Models\PurchaseOrderInvitation;
|
|
||||||
use App\Models\QuoteInvitation;
|
|
||||||
use App\Models\RecurringInvoiceInvitation;
|
|
||||||
use App\Utils\Traits\AppSetup;
|
use App\Utils\Traits\AppSetup;
|
||||||
use App\Utils\Traits\DesignCalculator;
|
use App\Models\QuoteInvitation;
|
||||||
|
use App\Models\CreditInvitation;
|
||||||
use App\Utils\Traits\MakesDates;
|
use App\Utils\Traits\MakesDates;
|
||||||
use Exception;
|
use App\Models\InvoiceInvitation;
|
||||||
use Illuminate\Support\Facades\App;
|
use Illuminate\Support\Facades\App;
|
||||||
use Illuminate\Support\Facades\Cache;
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
use App\Utils\Traits\DesignCalculator;
|
||||||
|
use App\Models\PurchaseOrderInvitation;
|
||||||
|
use App\Models\RecurringInvoiceInvitation;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Note the premise used here is that any currencies will be formatted back to the company currency and not
|
* Note the premise used here is that any currencies will be formatted back to the company currency and not
|
||||||
@@ -350,8 +351,13 @@ class VendorHtmlEngine
|
|||||||
$data['$signature'] = ['value' => $this->settings->email_signature ?: ' ', 'label' => ''];
|
$data['$signature'] = ['value' => $this->settings->email_signature ?: ' ', 'label' => ''];
|
||||||
$data['$emailSignature'] = &$data['$signature'];
|
$data['$emailSignature'] = &$data['$signature'];
|
||||||
|
|
||||||
$logo = $this->company->present()->logo_base64($this->settings);
|
if (Ninja::isHosted()) {
|
||||||
|
$logo = $this->company->present()->logo($this->settings);
|
||||||
|
} else {
|
||||||
|
$logo = $this->company->present()->logo_base64($this->settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
$data['$company.logo'] = ['value' => $logo ?: ' ', 'label' => ctrans('texts.logo')];
|
$data['$company.logo'] = ['value' => $logo ?: ' ', 'label' => ctrans('texts.logo')];
|
||||||
$data['$company_logo'] = &$data['$company.logo'];
|
$data['$company_logo'] = &$data['$company.logo'];
|
||||||
$data['$company1'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'company1', $this->settings->custom_value1, $this->vendor) ?: ' ', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'company1')];
|
$data['$company1'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'company1', $this->settings->custom_value1, $this->vendor) ?: ' ', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'company1')];
|
||||||
|
|||||||
@@ -152,6 +152,7 @@ return [
|
|||||||
'quickbooks_webhook' => [
|
'quickbooks_webhook' => [
|
||||||
'verifier_token' => env('QUICKBOOKS_VERIFIER_TOKEN', false),
|
'verifier_token' => env('QUICKBOOKS_VERIFIER_TOKEN', false),
|
||||||
],
|
],
|
||||||
|
|
||||||
'verifactu' => [
|
'verifactu' => [
|
||||||
'sender_nif' => env('VERIFACTU_SENDER_NIF', ''),
|
'sender_nif' => env('VERIFACTU_SENDER_NIF', ''),
|
||||||
'certificate' => env('VERIFACTU_CERTIFICATE', ''),
|
'certificate' => env('VERIFACTU_CERTIFICATE', ''),
|
||||||
@@ -159,6 +160,14 @@ return [
|
|||||||
'sender_name' => env('VERIFACTU_SENDER_NAME', 'CERTIFICADO FISICA PRUEBAS'),
|
'sender_name' => env('VERIFACTU_SENDER_NAME', 'CERTIFICADO FISICA PRUEBAS'),
|
||||||
'test_mode' => env('VERIFACTU_TEST_MODE', false),
|
'test_mode' => env('VERIFACTU_TEST_MODE', false),
|
||||||
],
|
],
|
||||||
|
'quickbooks' => [
|
||||||
|
'client_id' => env('QUICKBOOKS_CLIENT_ID', false),
|
||||||
|
'client_secret' => env('QUICKBOOKS_CLIENT_SECRET', false),
|
||||||
|
'redirect' => env('QUICKBOOKS_REDIRECT_URI'),
|
||||||
|
'test_redirect' => env('QUICKBOOKS_TEST_REDIRECT_URI'),
|
||||||
|
'env' => env('QUICKBOOKS_ENV', 'sandbox'),
|
||||||
|
'debug' => env('APP_DEBUG',false)
|
||||||
|
],
|
||||||
'cloudflare' => [
|
'cloudflare' => [
|
||||||
'zone_id' => env('CLOUDFLARE_SAAS_ZONE_ID', false),
|
'zone_id' => env('CLOUDFLARE_SAAS_ZONE_ID', false),
|
||||||
'api_token' => env('CLOUDFLARE_SAAS_API_TOKEN', false),
|
'api_token' => env('CLOUDFLARE_SAAS_API_TOKEN', false),
|
||||||
|
|||||||
@@ -5691,6 +5691,8 @@ $lang = array(
|
|||||||
'peppol_sending_success' => 'E-Invoice sent successfully!',
|
'peppol_sending_success' => 'E-Invoice sent successfully!',
|
||||||
'auto_generate' => 'Auto Generate',
|
'auto_generate' => 'Auto Generate',
|
||||||
'mollie_payment_pending' => 'Your payment is pending. Please wait for it to be processed. We will email you when it is completed.',
|
'mollie_payment_pending' => 'Your payment is pending. Please wait for it to be processed. We will email you when it is completed.',
|
||||||
|
'over_payment_helper' => 'Optional: you can pay more than the amount shown here. (ie tip, round up)',
|
||||||
|
'new_resource' => 'New Resource',
|
||||||
);
|
);
|
||||||
|
|
||||||
return $lang;
|
return $lang;
|
||||||
|
|||||||
@@ -8,14 +8,24 @@
|
|||||||
<dd class="text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2 flex flex-col">
|
<dd class="text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2 flex flex-col">
|
||||||
<template x-for="(invoice, index) in payableInvoices" :key="index">
|
<template x-for="(invoice, index) in payableInvoices" :key="index">
|
||||||
|
|
||||||
<div class="flex items-center mb-2">
|
<div class="mb-2">
|
||||||
<label>
|
<div class="flex items-center">
|
||||||
<span x-text="'{{ ctrans('texts.invoice') }} ' + invoice.number" class="mt-2"></span>
|
<label>
|
||||||
<span class="pr-2">{{ $currency->code }} ({{ $currency->symbol }})</span>
|
<span x-text="'{{ ctrans('texts.invoice') }} ' + invoice.number" class="mt-2"></span>
|
||||||
<input type="text" class="input mt-0 mr-4 relative" name="payable_invoices[]"
|
<span class="pr-2">{{ $currency->code }} ({{ $currency->symbol }})</span>
|
||||||
x-model="payableInvoices[index].formatted_amount" />
|
<input type="text" class="input mt-0 mr-4 relative" name="payable_invoices[]"
|
||||||
</label>
|
x-model="payableInvoices[index].formatted_amount" />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if($settings->client_portal_allow_over_payment)
|
||||||
|
<div class="mt-1">
|
||||||
|
<span class="text-xs text-gray-800 italic">{{ ctrans('texts.over_payment_helper') }}</span>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template x-if="errors.length > 0">
|
<template x-if="errors.length > 0">
|
||||||
@@ -26,6 +36,8 @@
|
|||||||
<span class="mt-1 text-sm text-gray-800">{{ ctrans('texts.minimum_payment') }}:
|
<span class="mt-1 text-sm text-gray-800">{{ ctrans('texts.minimum_payment') }}:
|
||||||
{{ $settings->client_portal_under_payment_minimum }}</span>
|
{{ $settings->client_portal_under_payment_minimum }}</span>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
|
|
||||||
</dd>
|
</dd>
|
||||||
|
|
||||||
<div class="bg-white px-4 py-5 flex items-center w-full justify-end space-x-3">
|
<div class="bg-white px-4 py-5 flex items-center w-full justify-end space-x-3">
|
||||||
|
|||||||
@@ -102,6 +102,12 @@
|
|||||||
{{ $invoice->client->currency()->code }} ({{ $invoice->client->currency()->symbol }})
|
{{ $invoice->client->currency()->code }} ({{ $invoice->client->currency()->symbol }})
|
||||||
{{ $invoice->partial > 0 ? $invoice->partial : $invoice->balance }}
|
{{ $invoice->partial > 0 ? $invoice->partial : $invoice->balance }}
|
||||||
</dd>
|
</dd>
|
||||||
|
|
||||||
|
@if($settings->client_portal_allow_over_payment)
|
||||||
|
<div class="mt-1">
|
||||||
|
<span class="mt-1 text-sm text-gray-800">{{ ctrans('texts.over_payment_helper') }}</span>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ use App\Http\Controllers\SystemLogController;
|
|||||||
use App\Http\Controllers\TwoFactorController;
|
use App\Http\Controllers\TwoFactorController;
|
||||||
use App\Http\Controllers\Auth\LoginController;
|
use App\Http\Controllers\Auth\LoginController;
|
||||||
use App\Http\Controllers\ImportJsonController;
|
use App\Http\Controllers\ImportJsonController;
|
||||||
|
use App\Http\Controllers\QuickbooksController;
|
||||||
use App\Http\Controllers\SelfUpdateController;
|
use App\Http\Controllers\SelfUpdateController;
|
||||||
use App\Http\Controllers\TaskStatusController;
|
use App\Http\Controllers\TaskStatusController;
|
||||||
use App\Http\Controllers\Bank\YodleeController;
|
use App\Http\Controllers\Bank\YodleeController;
|
||||||
@@ -336,8 +337,9 @@ Route::group(['middleware' => ['throttle:api', 'token_auth', 'valid_json','local
|
|||||||
Route::get('quote/{invitation_key}/download', [QuoteController::class, 'downloadPdf'])->name('quotes.downloadPdf');
|
Route::get('quote/{invitation_key}/download', [QuoteController::class, 'downloadPdf'])->name('quotes.downloadPdf');
|
||||||
Route::get('quote/{invitation_key}/download_e_quote', [QuoteController::class, 'downloadEQuote'])->name('quotes.downloadEQuote');
|
Route::get('quote/{invitation_key}/download_e_quote', [QuoteController::class, 'downloadEQuote'])->name('quotes.downloadEQuote');
|
||||||
|
|
||||||
Route::post('quickbooks/sync', [ImportQuickbooksController::class, 'sync'])->name('quickbooks.sync');
|
Route::post('quickbooks/sync', [QuickbooksController::class, 'sync'])->name('quickbooks.sync');
|
||||||
Route::post('quickbooks/configuration', [ImportQuickbooksController::class, 'configuration'])->name('quickbooks.configuration');
|
Route::post('quickbooks/configuration', [QuickbooksController::class, 'configuration'])->name('quickbooks.configuration');
|
||||||
|
Route::post('quickbooks/disconnect', [QuickbooksController::class, 'disconnect'])->name('quickbooks.disconnect');
|
||||||
|
|
||||||
Route::resource('recurring_expenses', RecurringExpenseController::class);
|
Route::resource('recurring_expenses', RecurringExpenseController::class);
|
||||||
Route::post('recurring_expenses/bulk', [RecurringExpenseController::class, 'bulk'])->name('recurring_expenses.bulk');
|
Route::post('recurring_expenses/bulk', [RecurringExpenseController::class, 'bulk'])->name('recurring_expenses.bulk');
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ class DesignApiTest extends TestCase
|
|||||||
|
|
||||||
$this->assertEquals($this->user->id, $d->user_id);
|
$this->assertEquals($this->user->id, $d->user_id);
|
||||||
$this->assertEquals($this->company->id, $d->company_id);
|
$this->assertEquals($this->company->id, $d->company_id);
|
||||||
$this->assertEquals($design->name.' clone '.date('Y-m-d H:i:s'), $d->name);
|
$this->assertStringContainsString($design->name.' clone ', $d->name);
|
||||||
// $dsd = Design::all()->pluck('name')->toArray();
|
// $dsd = Design::all()->pluck('name')->toArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -174,9 +174,11 @@ class SubscriptionApiTest extends TestCase
|
|||||||
|
|
||||||
// nlog($i->count());
|
// nlog($i->count());
|
||||||
// nlog($i->toArray());
|
// nlog($i->toArray());
|
||||||
}
|
|
||||||
|
|
||||||
$this->assertFalse($i);
|
$this->assertCount(0, $i);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
$this->assertFalse($i);
|
||||||
|
|
||||||
$this->travelTo($timezone_now->copy()->startOfDay());
|
$this->travelTo($timezone_now->copy()->startOfDay());
|
||||||
|
|
||||||
@@ -213,9 +215,9 @@ class SubscriptionApiTest extends TestCase
|
|||||||
->whereDate('due_date', '<=', now()->setTimezone($company->timezone()->name)->addDay()->startOfDay())
|
->whereDate('due_date', '<=', now()->setTimezone($company->timezone()->name)->addDay()->startOfDay())
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
}
|
$this->assertCount(0, $i);
|
||||||
|
|
||||||
$this->assertFalse($i);
|
}
|
||||||
|
|
||||||
$count = Invoice::whereNotNull('subscription_id')->whereIn('company_id', [$c2->id, $c->id])->count();
|
$count = Invoice::whereNotNull('subscription_id')->whereIn('company_id', [$c2->id, $c->id])->count();
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,299 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invoice Ninja (https://invoiceninja.com).
|
||||||
|
*
|
||||||
|
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2025. Invoice Ninja LLC (https://invoiceninja.com)
|
||||||
|
*
|
||||||
|
* @license https://www.elastic.co/licensing/elastic-license
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Tests\Unit\Services\Quickbooks\Transformers;
|
||||||
|
|
||||||
|
use Tests\TestCase;
|
||||||
|
use Tests\MockAccountData;
|
||||||
|
use App\Models\Company;
|
||||||
|
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||||
|
use App\Services\Quickbooks\Transformers\CompanyTransformer;
|
||||||
|
|
||||||
|
class CompanyTransformerTest extends TestCase
|
||||||
|
{
|
||||||
|
use MockAccountData;
|
||||||
|
use DatabaseTransactions;
|
||||||
|
|
||||||
|
private CompanyTransformer $transformer;
|
||||||
|
private array $qbCompanyInfo;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
|
||||||
|
$this->makeTestData();
|
||||||
|
|
||||||
|
$this->transformer = new CompanyTransformer($this->company);
|
||||||
|
|
||||||
|
// Mock QuickBooks IPPCompanyInfo structure based on the payload provided
|
||||||
|
$this->qbCompanyInfo = [
|
||||||
|
'Id' => '1',
|
||||||
|
'SyncToken' => '9',
|
||||||
|
'CompanyName' => 'Sandbox Company_US_1',
|
||||||
|
'LegalName' => 'Sandbox Company_US_1',
|
||||||
|
'CompanyAddr' => [
|
||||||
|
'Id' => '1',
|
||||||
|
'Line1' => '123 Sierra Way',
|
||||||
|
'Line2' => '',
|
||||||
|
'Line3' => '',
|
||||||
|
'Line4' => '',
|
||||||
|
'Line5' => '',
|
||||||
|
'City' => 'San Pablo',
|
||||||
|
'Country' => 'USA',
|
||||||
|
'CountryCode' => '',
|
||||||
|
'County' => '',
|
||||||
|
'CountrySubDivisionCode' => 'CA',
|
||||||
|
'PostalCode' => '87999',
|
||||||
|
'PostalCodeSuffix' => '',
|
||||||
|
],
|
||||||
|
'CustomerCommunicationAddr' => [
|
||||||
|
'Id' => '387',
|
||||||
|
'Line1' => '123 Sierra Way',
|
||||||
|
'Line2' => '',
|
||||||
|
'City' => 'San Pablo',
|
||||||
|
'Country' => '',
|
||||||
|
'CountryCode' => '',
|
||||||
|
'CountrySubDivisionCode' => 'CA',
|
||||||
|
'PostalCode' => '87999',
|
||||||
|
],
|
||||||
|
'LegalAddr' => [
|
||||||
|
'Id' => '386',
|
||||||
|
'Line1' => '123 Sierra Way',
|
||||||
|
'Line2' => '',
|
||||||
|
'City' => 'San Pablo',
|
||||||
|
'Country' => '',
|
||||||
|
'CountryCode' => '',
|
||||||
|
'CountrySubDivisionCode' => 'CA',
|
||||||
|
'PostalCode' => '87999',
|
||||||
|
],
|
||||||
|
'CompanyEmailAddr' => null,
|
||||||
|
'CustomerCommunicationEmailAddr' => [
|
||||||
|
'Id' => '',
|
||||||
|
'Address' => 'david@invoiceninja.com',
|
||||||
|
'Default' => null,
|
||||||
|
'Tag' => '',
|
||||||
|
],
|
||||||
|
'CompanyURL' => '',
|
||||||
|
'PrimaryPhone' => [
|
||||||
|
'Id' => '',
|
||||||
|
'DeviceType' => '',
|
||||||
|
'CountryCode' => '',
|
||||||
|
'AreaCode' => '',
|
||||||
|
'ExchangeCode' => '',
|
||||||
|
'Extension' => '',
|
||||||
|
'FreeFormNumber' => '4081234567',
|
||||||
|
'Default' => null,
|
||||||
|
'Tag' => '',
|
||||||
|
],
|
||||||
|
'Email' => [
|
||||||
|
'Id' => '',
|
||||||
|
'Address' => 'david@invoiceninja.com',
|
||||||
|
'Default' => null,
|
||||||
|
'Tag' => '',
|
||||||
|
],
|
||||||
|
'WebAddr' => '',
|
||||||
|
'Country' => 'US',
|
||||||
|
'DefaultTimeZone' => 'America/Los_Angeles',
|
||||||
|
'SupportedLanguages' => 'en',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testTransformerInstance(): void
|
||||||
|
{
|
||||||
|
$this->assertInstanceOf(CompanyTransformer::class, $this->transformer);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testTransformReturnsArray(): void
|
||||||
|
{
|
||||||
|
$result = $this->transformer->transform($this->qbCompanyInfo);
|
||||||
|
|
||||||
|
$this->assertIsArray($result);
|
||||||
|
$this->assertArrayHasKey('quickbooks', $result);
|
||||||
|
$this->assertArrayHasKey('settings', $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testQuickbooksDataStructure(): void
|
||||||
|
{
|
||||||
|
$result = $this->transformer->transform($this->qbCompanyInfo);
|
||||||
|
|
||||||
|
$this->assertArrayHasKey('companyName', $result['quickbooks']);
|
||||||
|
$this->assertEquals('Sandbox Company_US_1', $result['quickbooks']['companyName']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSettingsDataStructure(): void
|
||||||
|
{
|
||||||
|
$result = $this->transformer->transform($this->qbCompanyInfo);
|
||||||
|
|
||||||
|
$settings = $result['settings'];
|
||||||
|
|
||||||
|
$this->assertArrayHasKey('address1', $settings);
|
||||||
|
$this->assertArrayHasKey('address2', $settings);
|
||||||
|
$this->assertArrayHasKey('city', $settings);
|
||||||
|
$this->assertArrayHasKey('state', $settings);
|
||||||
|
$this->assertArrayHasKey('postal_code', $settings);
|
||||||
|
$this->assertArrayHasKey('country_id', $settings);
|
||||||
|
$this->assertArrayHasKey('phone', $settings);
|
||||||
|
$this->assertArrayHasKey('email', $settings);
|
||||||
|
$this->assertArrayHasKey('website', $settings);
|
||||||
|
$this->assertArrayHasKey('timezone_id', $settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAddressMapping(): void
|
||||||
|
{
|
||||||
|
$result = $this->transformer->transform($this->qbCompanyInfo);
|
||||||
|
|
||||||
|
$settings = $result['settings'];
|
||||||
|
|
||||||
|
// Should use CompanyAddr as primary
|
||||||
|
$this->assertEquals('123 Sierra Way', $settings['address1']);
|
||||||
|
$this->assertEquals('San Pablo', $settings['city']);
|
||||||
|
$this->assertEquals('CA', $settings['state']);
|
||||||
|
$this->assertEquals('87999', $settings['postal_code']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testContactInformationMapping(): void
|
||||||
|
{
|
||||||
|
$result = $this->transformer->transform($this->qbCompanyInfo);
|
||||||
|
|
||||||
|
$settings = $result['settings'];
|
||||||
|
|
||||||
|
$this->assertEquals('4081234567', $settings['phone']);
|
||||||
|
$this->assertEquals('david@invoiceninja.com', $settings['email']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testCountryResolution(): void
|
||||||
|
{
|
||||||
|
$result = $this->transformer->transform($this->qbCompanyInfo);
|
||||||
|
|
||||||
|
$settings = $result['settings'];
|
||||||
|
|
||||||
|
// Country should be resolved to a valid country_id
|
||||||
|
$this->assertNotEmpty($settings['country_id']);
|
||||||
|
$this->assertIsString($settings['country_id']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testTimezoneResolution(): void
|
||||||
|
{
|
||||||
|
$result = $this->transformer->transform($this->qbCompanyInfo);
|
||||||
|
|
||||||
|
$settings = $result['settings'];
|
||||||
|
|
||||||
|
// Timezone should be resolved to a valid timezone_id
|
||||||
|
$this->assertNotEmpty($settings['timezone_id']);
|
||||||
|
$this->assertIsString($settings['timezone_id']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testCanPersistQuickbooksData(): void
|
||||||
|
{
|
||||||
|
$result = $this->transformer->transform($this->qbCompanyInfo);
|
||||||
|
|
||||||
|
// Get fresh company instance
|
||||||
|
$company = Company::find($this->company->id);
|
||||||
|
|
||||||
|
// Update quickbooks data
|
||||||
|
$company->quickbooks->companyName = $result['quickbooks']['companyName'];
|
||||||
|
|
||||||
|
// Should not throw exception
|
||||||
|
$company->save();
|
||||||
|
|
||||||
|
// Verify it was saved
|
||||||
|
$company->refresh();
|
||||||
|
$this->assertEquals('Sandbox Company_US_1', $company->quickbooks->companyName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testCanPersistSettingsData(): void
|
||||||
|
{
|
||||||
|
$result = $this->transformer->transform($this->qbCompanyInfo);
|
||||||
|
|
||||||
|
// Get fresh company instance
|
||||||
|
$company = Company::find($this->company->id);
|
||||||
|
|
||||||
|
// Merge settings data
|
||||||
|
$company->saveSettings($result['settings'], $company);
|
||||||
|
|
||||||
|
// Should not throw exception
|
||||||
|
$company->save();
|
||||||
|
|
||||||
|
// Verify settings were saved
|
||||||
|
$company->refresh();
|
||||||
|
$this->assertEquals('123 Sierra Way', $company->settings->address1);
|
||||||
|
$this->assertEquals('San Pablo', $company->settings->city);
|
||||||
|
$this->assertEquals('CA', $company->settings->state);
|
||||||
|
$this->assertEquals('87999', $company->settings->postal_code);
|
||||||
|
$this->assertEquals('4081234567', $company->settings->phone);
|
||||||
|
$this->assertEquals('david@invoiceninja.com', $company->settings->email);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testCanPersistBothQuickbooksAndSettings(): void
|
||||||
|
{
|
||||||
|
$result = $this->transformer->transform($this->qbCompanyInfo);
|
||||||
|
|
||||||
|
// Get fresh company instance
|
||||||
|
$company = Company::find($this->company->id);
|
||||||
|
|
||||||
|
// Update both quickbooks and settings
|
||||||
|
$company->quickbooks->companyName = $result['quickbooks']['companyName'];
|
||||||
|
$company->saveSettings($result['settings'], $company);
|
||||||
|
|
||||||
|
// Should not throw exception
|
||||||
|
$company->save();
|
||||||
|
|
||||||
|
// Verify both were saved
|
||||||
|
$company->refresh();
|
||||||
|
$this->assertEquals('Sandbox Company_US_1', $company->quickbooks->companyName);
|
||||||
|
$this->assertEquals('123 Sierra Way', $company->settings->address1);
|
||||||
|
$this->assertEquals('david@invoiceninja.com', $company->settings->email);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAddressFallbackToLegalAddr(): void
|
||||||
|
{
|
||||||
|
// Remove CompanyAddr to test fallback
|
||||||
|
$qbData = $this->qbCompanyInfo;
|
||||||
|
unset($qbData['CompanyAddr']);
|
||||||
|
|
||||||
|
$result = $this->transformer->transform($qbData);
|
||||||
|
|
||||||
|
// Should fallback to LegalAddr
|
||||||
|
$this->assertEquals('123 Sierra Way', $result['settings']['address1']);
|
||||||
|
$this->assertEquals('San Pablo', $result['settings']['city']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testEmailFallback(): void
|
||||||
|
{
|
||||||
|
// Remove Email to test fallback to CustomerCommunicationEmailAddr
|
||||||
|
$qbData = $this->qbCompanyInfo;
|
||||||
|
unset($qbData['Email']);
|
||||||
|
|
||||||
|
$result = $this->transformer->transform($qbData);
|
||||||
|
|
||||||
|
// Should fallback to CustomerCommunicationEmailAddr
|
||||||
|
$this->assertEquals('david@invoiceninja.com', $result['settings']['email']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testHandlesEmptyData(): void
|
||||||
|
{
|
||||||
|
$emptyData = [
|
||||||
|
'CompanyName' => '',
|
||||||
|
'LegalName' => 'Test Legal Name',
|
||||||
|
];
|
||||||
|
|
||||||
|
$result = $this->transformer->transform($emptyData);
|
||||||
|
|
||||||
|
// Should use LegalName when CompanyName is empty
|
||||||
|
$this->assertEquals('Test Legal Name', $result['quickbooks']['companyName']);
|
||||||
|
|
||||||
|
// Settings should have empty strings for missing data
|
||||||
|
$this->assertEquals('', $result['settings']['address1']);
|
||||||
|
$this->assertEquals('', $result['settings']['phone']);
|
||||||
|
$this->assertEquals('', $result['settings']['email']);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user