diff --git a/app/Filters/ExpenseFilters.php b/app/Filters/ExpenseFilters.php index c86a848d0b0b..ac12b6356de9 100644 --- a/app/Filters/ExpenseFilters.php +++ b/app/Filters/ExpenseFilters.php @@ -218,6 +218,11 @@ class ExpenseFilters extends QueryFilters ->whereColumn('expense_categories.id', 'expenses.category_id'), $sort_col[1]); } + if ($sort_col[0] == 'payment_date' && in_array($sort_col[1], ['asc', 'desc'])) { + return $this->builder + ->orderByRaw('ISNULL(payment_date), payment_date '. $sort_col[1]); + } + if($sort_col[0] == 'number') { return $this->builder->orderByRaw("REGEXP_REPLACE(number,'[^0-9]+','')+0 " . $dir); } diff --git a/app/Http/Controllers/ImportQuickbooksController.php b/app/Http/Controllers/ImportQuickbooksController.php index 8f93651a7a25..be08f66333d4 100644 --- a/app/Http/Controllers/ImportQuickbooksController.php +++ b/app/Http/Controllers/ImportQuickbooksController.php @@ -1,30 +1,30 @@ 'Customer', - 'invoice' => 'Invoice', - 'product' => 'Item', - 'payment' => 'Payment' - ]; + // private array $import_entities = [ + // 'client' => 'Customer', + // 'invoice' => 'Invoice', + // 'product' => 'Item', + // 'payment' => 'Payment' + // ]; public function onAuthorized(AuthorizedQuickbooksRequest $request) { @@ -34,12 +34,11 @@ class ImportQuickbooksController extends BaseController $qb = new QuickbooksService($company); $realm = $request->query('realmId'); - $access_token_object = $qb->getAuth()->accessToken($request->query('code'), $realm); - nlog($access_token_object); //OAuth2AccessToken - $company->quickbooks = $access_token_object; - $company->save(); + $access_token_object = $qb->sdk()->accessTokenFromCode($request->query('code'), $realm); + $qb->sdk()->saveOAuthToken($access_token_object); + + return redirect(config('ninja.react_url')); - return response()->json(['message' => 'Success'], 200); //todo swapout for redirect to UI } /** @@ -53,8 +52,8 @@ class ImportQuickbooksController extends BaseController $company = $request->getCompany(); $qb = new QuickbooksService($company); - $authorizationUrl = $qb->getAuth()->getAuthorizationUrl(); - $state = $qb->getAuth()->getState(); + $authorizationUrl = $qb->sdk()->getAuthorizationUrl(); + $state = $qb->sdk()->getState(); Cache::put($state, $token, 190); @@ -63,29 +62,29 @@ class ImportQuickbooksController extends BaseController public function preimport(string $type, string $hash) { - // Check for authorization otherwise - // Create a reference - $data = [ - 'hash' => $hash, - 'type' => $type - ]; - $this->getData($data); + // // Check for authorization otherwise + // // Create a reference + // $data = [ + // 'hash' => $hash, + // 'type' => $type + // ]; + // $this->getData($data); } protected function getData($data) { - $entity = $this->import_entities[$data['type']]; - $cache_name = "{$data['hash']}-{$data['type']}"; - // TODO: Get or put cache or DB? - if(! Cache::has($cache_name)) { - $contents = call_user_func([$this->service, "fetch{$entity}s"]); - if($contents->isEmpty()) { - return; - } + // $entity = $this->import_entities[$data['type']]; + // $cache_name = "{$data['hash']}-{$data['type']}"; + // // TODO: Get or put cache or DB? + // if(! Cache::has($cache_name)) { + // $contents = call_user_func([$this->service, "fetch{$entity}s"]); + // if($contents->isEmpty()) { + // return; + // } - Cache::put($cache_name, base64_encode($contents->toJson()), 600); - } + // Cache::put($cache_name, base64_encode($contents->toJson()), 600); + // } } /** diff --git a/app/Import/Transformer/Quickbooks/ClientTransformer.php b/app/Import/Transformer/Quickbooks/ClientTransformer.php index 1a1d487652aa..7ad80ad8c737 100644 --- a/app/Import/Transformer/Quickbooks/ClientTransformer.php +++ b/app/Import/Transformer/Quickbooks/ClientTransformer.php @@ -67,7 +67,7 @@ class ClientTransformer extends BaseTransformer } $transformed_data = $this->preTransform($data); - $transformed_data['contacts'][0] = $this->getContacts($data)->toArray() + ['company_id' => $this->company->id ]; + $transformed_data['contacts'][0] = $this->getContacts($data)->toArray() + ['company_id' => $this->company->id, 'user_id' => $this->company->owner()->id ]; return $transformed_data; } @@ -79,7 +79,9 @@ class ClientTransformer extends BaseTransformer 'last_name' => $this->getString($data, 'FamilyName'), 'phone' => $this->getString($data, 'PrimaryPhone.FreeFormNumber'), 'email' => $this->getString($data, 'PrimaryEmailAddr.Address'), - 'company_id' => $this->company->id + 'company_id' => $this->company->id, + 'user_id' => $this->company->owner()->id, + 'send_email' => true, ]); } diff --git a/app/Repositories/Import/Quickbooks/CustomerRepository.php b/app/Repositories/Import/Quickbooks/CustomerRepository.php deleted file mode 100644 index 68b71b75017a..000000000000 --- a/app/Repositories/Import/Quickbooks/CustomerRepository.php +++ /dev/null @@ -1,11 +0,0 @@ -db = $db; - $this->transformer = $transfomer; - } - - public function count(): int - { - return $this->db->totalRecords($this->entity); - } - - public function all(): Collection - { - return $this->get($this->count()); - } - - public function get(int $max = 100): Collection - { - return $this->transformer->transform($this->db->fetchRecords($this->entity, $max), $this->entity); - } - - -} diff --git a/app/Services/Import/Quickbooks/Auth.php b/app/Services/Import/Quickbooks/Auth.php deleted file mode 100644 index edc3d605a8c0..000000000000 --- a/app/Services/Import/Quickbooks/Auth.php +++ /dev/null @@ -1,64 +0,0 @@ -sdk->accessToken($code, $realm); - } - - public function refreshToken(): array - { - // TODO: Get or put token in Cache or DB? - return $this->sdk->refreshToken(); - } - - public function getAuthorizationUrl(): string - { - return $this->sdk->getAuthorizationUrl(); - } - - public function getState(): string - { - return $this->sdk->getState(); - } - - public function getAccessToken(): array - { - $tokens = []; - // $token_store = new CompanyTokensRepository(); - // $tokens = $token_store->get(); - // if(empty($tokens)) { - // $token = $this->sdk->getAccessToken(); - // $access_token = $token->getAccessToken(); - // $realm = $token->getRealmID(); - // $refresh_token = $token->getRefreshToken(); - // $access_token_expires = $token->getAccessTokenExpiresAt(); - // $refresh_token_expires = $token->getRefreshTokenExpiresAt(); - // $tokens = compact('access_token', 'refresh_token','access_token_expires', 'refresh_token_expires','realm'); - // } - - return $tokens; - } - - public function getRefreshToken(): array - { - return $this->getAccessToken(); - } -} diff --git a/app/Services/Import/Quickbooks/Contracts/SdkInterface.php b/app/Services/Import/Quickbooks/Contracts/SdkInterface.php deleted file mode 100644 index 48ed4b32e84f..000000000000 --- a/app/Services/Import/Quickbooks/Contracts/SdkInterface.php +++ /dev/null @@ -1,14 +0,0 @@ -init() - ->auth(); + $this->init(); } private function init(): self { - $this->sdk = DataService::Configure([ + $config = [ 'ClientID' => config('services.quickbooks.client_id'), 'ClientSecret' => config('services.quickbooks.client_secret'), 'auth_mode' => 'oauth2', 'scope' => "com.intuit.quickbooks.accounting", - 'RedirectURI' => 'https://developer.intuit.com/v2/OAuth2Playground/RedirectUrl', - // 'RedirectURI' => route('quickbooks.authorized'), - ]); + // 'RedirectURI' => 'https://developer.intuit.com/v2/OAuth2Playground/RedirectUrl', + 'RedirectURI' => $this->testMode ? 'https://above-distinctly-teal.ngrok-free.app/quickbooks/authorized' : 'https://invoicing.co/quickbooks/authorized', + 'baseUrl' => $this->testMode ? CoreConstants::SANDBOX_DEVELOPMENT : CoreConstants::QBO_BASEURL, + ]; - // if (env('APP_DEBUG')) { - // $sdk->setLogLocation(storage_path("logs/quickbooks.log")); - // $sdk->enableLog(); - // } + $merged = array_merge($config, $this->ninjaAccessToken()); + + $this->sdk = DataService::Configure($merged); + + $this->sdk->setLogLocation(storage_path("logs/quickbooks.log")); + $this->sdk->enableLog(); $this->sdk->setMinorVersion("73"); $this->sdk->throwExceptionOnError(true); @@ -52,12 +55,13 @@ class QuickbooksService return $this; } - private function auth(): self + private function ninjaAccessToken() { - $wrapper = new SdkWrapper($this->sdk); - $this->auth = new Auth($wrapper); - - return $this; + return isset($this->company->quickbooks->accessTokenKey) ? [ + 'accessTokenKey' => $this->company->quickbooks->accessTokenKey, + 'refreshTokenKey' => $this->company->quickbooks->refresh_token, + 'QBORealmID' => $this->company->quickbooks->realmID, + ] : []; } public function getSdk(): DataService @@ -65,8 +69,9 @@ class QuickbooksService return $this->sdk; } - public function getAuth(): Auth + public function sdk(): SdkWrapper { - return $this->auth; + return new SdkWrapper($this->sdk, $this->company); } + } diff --git a/app/Services/Import/Quickbooks/Repositories/CompanyTokensRepository.php b/app/Services/Import/Quickbooks/Repositories/CompanyTokensRepository.php deleted file mode 100644 index 92aef0c8e9e3..000000000000 --- a/app/Services/Import/Quickbooks/Repositories/CompanyTokensRepository.php +++ /dev/null @@ -1,78 +0,0 @@ -company_key = $key ?? auth()->user->company()->company_key ?? null; - $this->store_key .= $key; - $this->setCompanyDbByKey(); - } - - public function save(array $tokens) - { - $this->updateAccessToken($tokens['access_token'], $tokens['access_token_expires']); - $this->updateRefreshToken($tokens['refresh_token'], $tokens['refresh_token_expires'], $tokens['realm']); - } - - - public function findByCompanyKey(): ?Company - { - return Company::where('company_key', $this->company_key)->first(); - } - - public function setCompanyDbByKey() - { - MultiDB::findAndSetDbByCompanyKey($this->company_key); - } - - public function get() - { - return $this->getAccessToken() + $this->getRefreshToken(); - } - - - protected function updateRefreshToken(string $token, string $expires, string $realm) - { - DB::table('companies') - ->where('company_key', $this->company_key) - ->update(['quickbooks_refresh_token' => $token, - 'quickbooks_realm_id' => $realm, - 'quickbooks_refresh_expires' => $expires ]); - } - - protected function updateAccessToken(string $token, string $expires) - { - - Cache::put([$this->store_key => $token], $expires); - } - - protected function getAccessToken() - { - $result = Cache::get($this->store_key); - - return $result ? ['access_token' => $result] : []; - } - - protected function getRefreshToken() - { - $result = (array) DB::table('companies') - ->select('quickbooks_refresh_token', 'quickbooks_realm_id') - ->where('company_key', $this->company_key) - ->where('quickbooks_refresh_expires', '>', now()) - ->first(); - - return $result ? array_combine(['refresh_token','realm'], array_values($result)) : []; - } - -} diff --git a/app/Services/Import/Quickbooks/SdkWrapper.php b/app/Services/Import/Quickbooks/SdkWrapper.php index 1cd8210c48ae..493719144cec 100644 --- a/app/Services/Import/Quickbooks/SdkWrapper.php +++ b/app/Services/Import/Quickbooks/SdkWrapper.php @@ -1,79 +1,162 @@ sdk = $sdk; + $this->init(); + } + + private function init(): self + { + + isset($this->company->quickbooks->accessTokenKey) ? $this->setNinjaAccessToken($this->company->quickbooks) : null; + + return $this; + } public function getAuthorizationUrl(): string { - return ($this->sdk->getOAuth2LoginHelper())->getAuthorizationCodeURL(); + return $this->sdk->getOAuth2LoginHelper()->getAuthorizationCodeURL(); } public function getState(): string { - return ($this->sdk->getOAuth2LoginHelper())->getState(); + return $this->sdk->getOAuth2LoginHelper()->getState(); } - public function getAccessToken() + public function getRefreshToken(): string { - return $this->getTokens(); + return $this->accessToken()->getRefreshToken(); } - - public function getRefreshToken(): array + + public function company() { - return $this->getTokens(); - } + nlog("getting company info"); + // nlog($this->sdk->getAccessToken()); - public function accessToken(string $code, string $realm): array + return $this->sdk->getCompanyInfo(); + } + /* + accessTokenKey + tokenType + refresh_token + accessTokenExpiresAt + refreshTokenExpiresAt + accessTokenValidationPeriod + refreshTokenValidationPeriod + clientID + clientSecret + realmID + baseURL + */ + public function accessTokenFromCode(string $code, string $realm): OAuth2AccessToken { - $token = ($this->sdk->getOAuth2LoginHelper())->exchangeAuthorizationCodeForToken($code, $realm); + $token = $this->sdk->getOAuth2LoginHelper()->exchangeAuthorizationCodeForToken($code, $realm); - return $this->getTokens(); + $this->setAccessToken($token); + + return $this->accessToken(); } - - private function getTokens() + + /** + * Set Stored NinjaAccessToken + * + * @param mixed $token_object + * @return self + */ + public function setNinjaAccessToken(mixed $token_object): self { + $token = new OAuth2AccessToken( + config('services.quickbooks.client_id'), + config('services.quickbooks.client_secret'), + $token_object->accessTokenKey, + $token_object->refresh_token, + 3600, + 8726400 + ); - $token = ($this->sdk->getOAuth2LoginHelper())->getAccessToken(); - return $token; + $token->setAccessTokenExpiresAt($token_object->accessTokenExpiresAt); + $token->setRefreshTokenExpiresAt($token_object->refreshTokenExpiresAt); + $token->setAccessTokenValidationPeriodInSeconds(3600); + $token->setRefreshTokenValidationPeriodInSeconds(8726400); - // $access_token = $token->getAccessToken(); - // $refresh_token = $token->getRefreshToken(); - // $access_token_expires = $token->getAccessTokenExpiresAt(); - // $refresh_token_expires = $token->getRefreshTokenExpiresAt(); + $this->setAccessToken($token); - // return compact('access_token', 'refresh_token','access_token_expires', 'refresh_token_expires'); + if($token_object->accessTokenExpiresAt < time()){ + $new_token = $this->sdk->getOAuth2LoginHelper()->refreshToken(); + + $this->setAccessToken($new_token); + $this->saveOAuthToken($this->accessToken()); + } + + return $this; } - - public function refreshToken(): array + + /** + * SetsAccessToken + * + * @param OAuth2AccessToken $token + * @return self + */ + public function setAccessToken(OAuth2AccessToken $token): self { - $token = ($this->sdk->getOAuth2LoginHelper())->refreshToken(); - $this->sdk = $this->sdk->updateOAuth2Token($token); + // $this->sdk = $this->sdk->updateOAuth2Token($token); - return $this->getTokens(); + $this->token = $token; + + return $this; } - public function handleCallbacks(array $data): void + public function accessToken(): OAuth2AccessToken { - + return $this->token; } + public function saveOAuthToken(OAuth2AccessToken $token): void + { + $obj = new \stdClass(); + $obj->accessTokenKey = $token->getAccessToken(); + $obj->refresh_token = $token->getRefreshToken(); + $obj->accessTokenExpiresAt = Carbon::createFromFormat('Y/m/d H:i:s', $token->getAccessTokenExpiresAt())->timestamp; //@phpstan-ignore-line - QB phpdoc wrong types!! + $obj->refreshTokenExpiresAt = Carbon::createFromFormat('Y/m/d H:i:s', $token->getRefreshTokenExpiresAt())->timestamp; //@phpstan-ignore-line - QB phpdoc wrong types!! + + $obj->realmID = $token->getRealmID(); + $obj->baseURL = $token->getBaseURL(); + + $this->company->quickbooks = $obj; + $this->company->save(); + } + + + /// Data Access /// + public function totalRecords(string $entity): int { - return $this->sdk->Query("select count(*) from $entity"); + return (int)$this->sdk->Query("select count(*) from $entity"); } private function queryData(string $query, int $start = 1, $limit = 100): array diff --git a/app/Services/Import/Quickbooks/Service.php b/app/Services/Import/Quickbooks/Service.php deleted file mode 100644 index 5002bd9e16af..000000000000 --- a/app/Services/Import/Quickbooks/Service.php +++ /dev/null @@ -1,89 +0,0 @@ -sdk = $quickbooks; - } - - public function getOAuth(): Auth - { - return new Auth($this->sdk); - } - - public function getAccessToken(): array - { - return $this->getOAuth()->getAccessToken(); - } - - public function getRefreshToken(): array - { - // TODO: Check if token is Cached otherwise fetch a new one and Cache token and expire - return $this->getAccessToken(); - } - /** - * fetch QuickBooks invoice records - * @param int $max The maximum records to fetch. Default 100 - * @return Illuminate\Support\Collection; - */ - public function fetchInvoices(int $max = 100): Collection - { - return $this->fetchRecords('Invoice', $max) ; - } - - /** - * fetch QuickBooks payment records - * @param int $max The maximum records to fetch. Default 100 - * @return Illuminate\Support\Collection; - */ - public function fetchPayments(int $max = 100): Collection - { - return $this->fetchRecords('Payment', $max) ; - } - - /** - * fetch QuickBooks product records - * @param int $max The maximum records to fetch. Default 100 - * @return Illuminate\Support\Collection; - */ - public function fetchItems(int $max = 100): Collection - { - return $this->fetchRecords('Item', $max) ; - } - - protected function fetchRecords(string $entity, $max = 100): Collection - { - return (self::RepositoryFactory($entity))->get($max); - } - - private static function RepositoryFactory(string $entity): RepositoryInterface - { - return app("\\App\\Repositories\\Import\Quickbooks\\{$entity}Repository"); - } - - /** - * fetch QuickBooks customer records - * @param int $max The maximum records to fetch. Default 100 - * @return Illuminate\Support\Collection; - */ - public function fetchCustomers(int $max = 100): Collection - { - return $this->fetchRecords('Customer', $max) ; - } - - public function totalRecords(string $entity): int - { - return (self::RepositoryFactory($entity))->count(); - } -} diff --git a/lang/en/texts.php b/lang/en/texts.php index fa77340579ce..55ded895bd16 100644 --- a/lang/en/texts.php +++ b/lang/en/texts.php @@ -5319,6 +5319,8 @@ $lang = array( 'one_page_checkout' => 'One-Page Checkout', 'one_page_checkout_help' => 'Enable the new single page payment flow', 'applies_to' => 'Applies To', + 'accept_purchase_order' => 'Accept Purchase Order', + 'round_to_seconds' => 'Round To Seconds', ); return $lang; diff --git a/lang/nl/texts.php b/lang/nl/texts.php index 1880ab664fa8..0745fcf96c1f 100644 --- a/lang/nl/texts.php +++ b/lang/nl/texts.php @@ -4847,7 +4847,7 @@ E-mail: :email
', 'failed' => 'Mislukt', 'client_contacts' => 'Klantencontacten', 'sync_from' => 'Synchroniseren van', - 'gateway_payment_text' => 'Facturen: :invoices voor :amount voor opdrachtgever :client', + 'gateway_payment_text' => 'Factuur/facturen: :invoices voor :amount voor opdrachtgever :client', 'gateway_payment_text_no_invoice' => 'Betaling zonder factuur voor bedrag :amount voor opdrachtgever :client', 'click_to_variables' => 'Klik hier om alle variabelen te zien.', 'ship_to' => 'Verzend naar', @@ -5314,6 +5314,11 @@ E-mail: :email
', 'comments_only' => 'Alleen opmerkingen', 'payment_balance_on_file' => 'Huidig saldo', 'ubl_email_attachment_help' => 'Voor meer e-factuurinstellingen, navigeer naar :here', + 'stop_task_to_add_task_entry' => 'U moet de taak stoppen alvorens een nieuw item toe te voegen.', + 'xml_file' => 'XML-bestand', + 'one_page_checkout' => 'Afrekenen op één pagina', + 'one_page_checkout_help' => 'Schakel de nieuwe betaalflow \'afrekenen op één pagina\' in', + 'applies_to' => 'Is van toepassing op', ); return $lang; diff --git a/routes/web.php b/routes/web.php index 4f22c1181435..c940d0fb1d8c 100644 --- a/routes/web.php +++ b/routes/web.php @@ -6,7 +6,6 @@ use App\Http\Controllers\Auth\ResetPasswordController; use App\Http\Controllers\Bank\NordigenController; use App\Http\Controllers\Bank\YodleeController; use App\Http\Controllers\BaseController; -use App\Http\Controllers\ImportQuickbooksController; use App\Http\Controllers\ClientPortal\ApplePayDomainController; use App\Http\Controllers\Gateways\Checkout3dsController; use App\Http\Controllers\Gateways\GoCardlessController;