Transform company

This commit is contained in:
David Bomba
2026-01-27 20:36:26 +11:00
parent 0c2e5f021f
commit 3eb147a63c
8 changed files with 193 additions and 7 deletions

View File

@@ -20,15 +20,23 @@ use App\Services\Quickbooks\QuickbooksService;
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)
{
MultiDB::findAndSetDbByCompanyKey($request->getTokenContent()['company_key']);
$company = $request->getCompany();
$qb = new QuickbooksService($company);
$authorizationUrl = $qb->sdk()->getAuthorizationUrl();
@@ -39,18 +47,45 @@ class ImportQuickbooksController extends BaseController
return redirect()->to($authorizationUrl);
}
/**
* onAuthorized
*
* Handles the callback from Quickbooks after authorization.
*
* @param AuthorizedQuickbooksRequest $request
* @return RedirectResponse
*/
public function onAuthorized(AuthorizedQuickbooksRequest $request)
{
nlog($request->all());
MultiDB::findAndSetDbByCompanyKey($request->getTokenContent()['company_key']);
$company = $request->getCompany();
$qb = new QuickbooksService($company);
$realm = $request->query('realmId');
nlog($realm);
$access_token_object = $qb->sdk()->accessTokenFromCode($request->query('code'), $realm);
nlog($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'));
}

View File

@@ -14,9 +14,10 @@ namespace App\Http\Controllers;
use App\Libraries\MultiDB;
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\DisconnectQuickbooksRequest;
use App\Http\Requests\Quickbooks\SyncQuickbooksRequest;
class QuickbooksController extends BaseController
{
@@ -53,7 +54,9 @@ class QuickbooksController extends BaseController
$company = $user->company();
$qb = new QuickbooksService($company);
$qb->sdk()->revokeAccessToken();
$rs = $qb->sdk()->revokeAccessToken();
nlog($rs);
$company->quickbooks = null;
$company->save();

View File

@@ -69,7 +69,7 @@ class QuickbooksService
'ClientSecret' => config('services.quickbooks.client_secret'),
'auth_mode' => 'oauth2',
'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,
];
@@ -134,6 +134,24 @@ class QuickbooksService
// 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
{

View File

@@ -55,6 +55,11 @@ class SdkWrapper
return $this->accessToken()->getRefreshToken();
}
public function revokeAccessToken()
{
return $this->sdk->getOAuth2LoginHelper()->revokeToken($this->accessToken()->getAccessToken());
}
public function company()
{
return $this->sdk->getCompanyInfo();

View File

@@ -50,6 +50,21 @@ class BaseTransformer
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)
{
return is_null(($c = $this->getString($data, $field))) ? null : $this->getCountryId($c);

View 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 : '';
}
}

View File

@@ -152,6 +152,7 @@ return [
'quickbooks_webhook' => [
'verifier_token' => env('QUICKBOOKS_VERIFIER_TOKEN', false),
],
'verifactu' => [
'sender_nif' => env('VERIFACTU_SENDER_NIF', ''),
'certificate' => env('VERIFACTU_CERTIFICATE', ''),
@@ -159,6 +160,14 @@ return [
'sender_name' => env('VERIFACTU_SENDER_NAME', 'CERTIFICADO FISICA PRUEBAS'),
'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' => [
'zone_id' => env('CLOUDFLARE_SAAS_ZONE_ID', false),
'api_token' => env('CLOUDFLARE_SAAS_API_TOKEN', false),

View File

@@ -61,6 +61,7 @@ use App\Http\Controllers\SystemLogController;
use App\Http\Controllers\TwoFactorController;
use App\Http\Controllers\Auth\LoginController;
use App\Http\Controllers\ImportJsonController;
use App\Http\Controllers\QuickbooksController;
use App\Http\Controllers\SelfUpdateController;
use App\Http\Controllers\TaskStatusController;
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_e_quote', [QuoteController::class, 'downloadEQuote'])->name('quotes.downloadEQuote');
Route::post('quickbooks/sync', [ImportQuickbooksController::class, 'sync'])->name('quickbooks.sync');
Route::post('quickbooks/configuration', [ImportQuickbooksController::class, 'configuration'])->name('quickbooks.configuration');
Route::post('quickbooks/sync', [QuickbooksController::class, 'sync'])->name('quickbooks.sync');
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::post('recurring_expenses/bulk', [RecurringExpenseController::class, 'bulk'])->name('recurring_expenses.bulk');