diff --git a/app/Http/Controllers/ImportQuickbooksController.php b/app/Http/Controllers/ImportQuickbooksController.php index 63c6f1e7dc..bc097e754c 100644 --- a/app/Http/Controllers/ImportQuickbooksController.php +++ b/app/Http/Controllers/ImportQuickbooksController.php @@ -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')); } diff --git a/app/Http/Controllers/QuickbooksController.php b/app/Http/Controllers/QuickbooksController.php index cd2451b01d..5ffffba2e6 100644 --- a/app/Http/Controllers/QuickbooksController.php +++ b/app/Http/Controllers/QuickbooksController.php @@ -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(); diff --git a/app/Services/Quickbooks/QuickbooksService.php b/app/Services/Quickbooks/QuickbooksService.php index 96174708f7..7a91a2d5d7 100644 --- a/app/Services/Quickbooks/QuickbooksService.php +++ b/app/Services/Quickbooks/QuickbooksService.php @@ -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 { diff --git a/app/Services/Quickbooks/SdkWrapper.php b/app/Services/Quickbooks/SdkWrapper.php index 26a14d615a..50871eb996 100644 --- a/app/Services/Quickbooks/SdkWrapper.php +++ b/app/Services/Quickbooks/SdkWrapper.php @@ -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(); diff --git a/app/Services/Quickbooks/Transformers/BaseTransformer.php b/app/Services/Quickbooks/Transformers/BaseTransformer.php index 52d4ae39ac..408ddd25ba 100644 --- a/app/Services/Quickbooks/Transformers/BaseTransformer.php +++ b/app/Services/Quickbooks/Transformers/BaseTransformer.php @@ -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); diff --git a/app/Services/Quickbooks/Transformers/CompanyTransformer.php b/app/Services/Quickbooks/Transformers/CompanyTransformer.php new file mode 100644 index 0000000000..b9d210715b --- /dev/null +++ b/app/Services/Quickbooks/Transformers/CompanyTransformer.php @@ -0,0 +1,99 @@ +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, settings: array} + */ + 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 : ''; + } +} diff --git a/config/services.php b/config/services.php index 95ec894333..7e762ee7f3 100644 --- a/config/services.php +++ b/config/services.php @@ -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), diff --git a/routes/api.php b/routes/api.php index cb345e579d..3d6bd096df 100644 --- a/routes/api.php +++ b/routes/api.php @@ -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');