diff --git a/app/Casts/QuickbooksSettingsCast.php b/app/Casts/QuickbooksSettingsCast.php new file mode 100644 index 000000000000..8e0ecb211d06 --- /dev/null +++ b/app/Casts/QuickbooksSettingsCast.php @@ -0,0 +1,47 @@ +accessTokenKey = $data['accessTokenKey']; + $qb->refresh_token = $data['refresh_token']; + $qb->realmID = $data['realmID']; + $qb->accessTokenExpiresAt = $data['accessTokenExpiresAt']; + $qb->refreshTokenExpiresAt = $data['refreshTokenExpiresAt']; + $qb->settings = $data['settings'] ?? []; + + return $qb; + } + + public function set($model, string $key, $value, array $attributes) + { + return [ + $key => json_encode([ + 'accessTokenKey' => $value->accessTokenKey, + 'refresh_token' => $value->refresh_token, + 'realmID' => $value->realmID, + 'accessTokenExpiresAt' => $value->accessTokenExpiresAt, + 'refreshTokenExpiresAt' => $value->refreshTokenExpiresAt, + 'settings' => $value->settings, + ]) + ]; + } +} diff --git a/app/DataMapper/QuickbooksSettings.php b/app/DataMapper/QuickbooksSettings.php index 671112689276..553215f02d13 100644 --- a/app/DataMapper/QuickbooksSettings.php +++ b/app/DataMapper/QuickbooksSettings.php @@ -11,10 +11,13 @@ namespace App\DataMapper; +use Illuminate\Contracts\Database\Eloquent\Castable; +use App\Casts\QuickbooksSettingsCast; + /** * QuickbooksSettings. */ -class QuickbooksSettings +class QuickbooksSettings implements Castable { public string $accessTokenKey; @@ -30,7 +33,27 @@ class QuickbooksSettings * entity client,invoice,quote,purchase_order,vendor,payment * sync true/false * update_record true/false - * direction push/pull/birection + * direction push/pull/birectional * */ - public array $settings = []; + public array $settings = [ + 'client' => ['sync' => true, 'update_record' => true, 'direction' => 'bidirectional'], + 'vendor' => ['sync' => true, 'update_record' => true, 'direction' => 'bidirectional'], + 'invoice' => ['sync' => true, 'update_record' => true, 'direction' => 'bidirectional'], + 'quote' => ['sync' => true, 'update_record' => true, 'direction' => 'bidirectional'], + 'purchase_order' => ['sync' => true, 'update_record' => true, 'direction' => 'bidirectional'], + 'product' => ['sync' => true, 'update_record' => true, 'direction' => 'bidirectional'], + 'payment' => ['sync' => true, 'update_record' => true, 'direction' => 'bidirectional'], + ]; + + + /** + * Get the name of the caster class to use when casting from / to this cast target. + * + * @param array $arguments + */ + public static function castUsing(array $arguments): string + { + return QuickbooksSettingsCast::class; + } + } diff --git a/app/Models/Company.php b/app/Models/Company.php index eeb8263afab9..b996e47d835c 100644 --- a/app/Models/Company.php +++ b/app/Models/Company.php @@ -13,6 +13,7 @@ namespace App\Models; use App\Casts\EncryptedCast; use App\DataMapper\CompanySettings; +use App\DataMapper\QuickbooksSettings; use App\Models\Presenters\CompanyPresenter; use App\Services\Company\CompanyService; use App\Services\Notification\NotificationService; @@ -392,7 +393,7 @@ class Company extends BaseModel 'smtp_username' => 'encrypted', 'smtp_password' => 'encrypted', 'e_invoice' => 'object', - 'quickbooks' => 'object', + 'quickbooks' => QuickbooksSettings::class, ]; protected $with = []; diff --git a/app/Services/Import/Quickbooks/QuickbooksService.php b/app/Services/Import/Quickbooks/QuickbooksService.php index 696c43398cce..480c9696e097 100644 --- a/app/Services/Import/Quickbooks/QuickbooksService.php +++ b/app/Services/Import/Quickbooks/QuickbooksService.php @@ -11,6 +11,7 @@ namespace App\Services\Import\Quickbooks; +use App\Factory\ClientContactFactory; use App\Factory\ClientFactory; use App\Models\Client; use App\Models\Company; @@ -36,7 +37,7 @@ class QuickbooksService private bool $testMode = true; - private array $settings = []; + private mixed $settings; public function __construct(private Company $company) { @@ -89,24 +90,44 @@ class QuickbooksService * * @return void */ - public function sync() + public function syncFromQb() { //syncable_records. - foreach($this->entities as $entity) + foreach($this->entities as $key => $entity) { + if(!$this->syncGate($key, 'pull')) + continue; $records = $this->sdk()->fetchRecords($entity); - $this->processEntitySync($entity, $records); - + nlog($records); + + $this->processEntitySync($key, $records); } } + private function syncGate(string $entity, string $direction): bool + { + return (bool) $this->settings[$entity]['sync'] && in_array($this->settings[$entity]['direction'], [$direction,'bidirectional']); + } + + private function updateGate(string $entity): bool + { + return (bool) $this->settings[$entity]['sync'] && $this->settings[$entity]['update_record']; + } + + private function harvestQbEntityName(string $entity): string + { + return $this->entities[$entity]; + } + private function processEntitySync(string $entity, $records) { + nlog($entity); + nlog($records); match($entity){ 'client' => $this->syncQbToNinjaClients($records), // 'vendor' => $this->syncQbToNinjaClients($records), @@ -115,32 +136,57 @@ class QuickbooksService // 'purchase_order' => $this->syncInvoices($records), // 'payment' => $this->syncPayment($records), // 'product' => $this->syncItem($records), + default => false, }; } private function syncQbToNinjaClients(array $records) { + nlog("qb => ninja"); + + $client_transformer = new ClientTransformer(); + foreach($records as $record) { - $ninja_client_data = new ClientTransformer($record); + $ninja_client_data = $client_transformer->qbToNinja($record); if($client = $this->findClient($ninja_client_data)) { $client->fill($ninja_client_data[0]); + $client->saveQuietly(); + + $contact = $client->contacts()->where('email', $ninja_client_data[1]['email'])->first(); + + if(!$contact) + { + $contact = ClientContactFactory::create($this->company->id, $this->company->owner()->id); + $contact->client_id = $client->id; + $contact->send_email = true; + $contact->is_primary = true; + $contact->fill($ninja_client_data[1]); + $contact->saveQuietly(); + } + elseif($this->updateGate('client')){ + $contact->fill($ninja_client_data[1]); + $contact->saveQuietly(); + } + } } } - private function findClient(array $qb_data) + private function findClient(array $qb_data) :?Client { $client = $qb_data[0]; $contact = $qb_data[1]; $client_meta = $qb_data[2]; + nlog($qb_data); + $search = Client::query() ->withTrashed() - ->where('company', $this->company->id) + ->where('company_id', $this->company->id) ->where(function ($q) use ($client, $client_meta, $contact){ $q->where('client_hash', $client_meta['client_hash']) @@ -160,7 +206,7 @@ class QuickbooksService return $client; } elseif($search->count() == 1) { - // ? sync / update + return $this->settings['client']['update_record'] ? $search->first() : null; } else { //potentially multiple matching clients? diff --git a/app/Services/Import/Quickbooks/SdkWrapper.php b/app/Services/Import/Quickbooks/SdkWrapper.php index 8441c83587f0..61fe577c723e 100644 --- a/app/Services/Import/Quickbooks/SdkWrapper.php +++ b/app/Services/Import/Quickbooks/SdkWrapper.php @@ -11,6 +11,7 @@ namespace App\Services\Import\Quickbooks; +use App\DataMapper\QuickbooksSettings; use Carbon\Carbon; use App\Models\Company; use QuickBooksOnline\API\DataService\DataService; @@ -82,10 +83,10 @@ class SdkWrapper /** * Set Stored NinjaAccessToken * - * @param mixed $token_object + * @param QuickbooksSettings $token_object * @return self */ - public function setNinjaAccessToken(mixed $token_object): self + public function setNinjaAccessToken(QuickbooksSettings $token_object): self { $token = new OAuth2AccessToken( config('services.quickbooks.client_id'), @@ -133,7 +134,7 @@ class SdkWrapper public function saveOAuthToken(OAuth2AccessToken $token): void { - $obj = new \stdClass(); + $obj = $this->company->quickbooks ?? new QuickbooksSettings(); $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!! diff --git a/app/Services/Import/Quickbooks/Transfomers/ClientTransformer.php b/app/Services/Import/Quickbooks/Transformers/ClientTransformer.php similarity index 82% rename from app/Services/Import/Quickbooks/Transfomers/ClientTransformer.php rename to app/Services/Import/Quickbooks/Transformers/ClientTransformer.php index 246a65636c2b..e355f2cf0c76 100644 --- a/app/Services/Import/Quickbooks/Transfomers/ClientTransformer.php +++ b/app/Services/Import/Quickbooks/Transformers/ClientTransformer.php @@ -20,12 +20,21 @@ use App\DataMapper\ClientSettings; class ClientTransformer { - public function __invoke($qb_data): array + public function __construct() + { + } + + public function qbToNinja(mixed $qb_data) { return $this->transform($qb_data); } - public function transform($data): array + public function ninjaToQb() + { + + } + + public function transform(mixed $data): array { $contact = [ @@ -33,7 +42,7 @@ class ClientTransformer 'last_name' => data_get($data, 'FamilyName'), 'phone' => data_get($data, 'PrimaryPhone.FreeFormNumber'), 'email' => data_get($data, 'PrimaryEmailAddr.Address'), - ]; + ]; $client = [ 'name' => data_get($data,'CompanyName', ''), @@ -49,7 +58,7 @@ class ClientTransformer 'shipping_country_id' => $this->resolveCountry(data_get($data, 'ShipAddr.Country', '')), 'shipping_state' => data_get($data, 'ShipAddr.CountrySubDivisionCode', ''), 'shipping_postal_code' => data_get($data, 'BillAddr.PostalCode', ''), - 'id_number' => data_get($data, 'Id', ''), + 'id_number' => data_get($data, 'Id.value', ''), ]; $settings = ClientSettings::defaults(); @@ -65,16 +74,20 @@ class ClientTransformer private function resolveCountry(string $iso_3_code) { - return (string) app('countries')->first(function ($c) use ($iso_3_code){ + $country = app('countries')->first(function ($c) use ($iso_3_code){ return $c->iso_3166_3 == $iso_3_code; - })->id ?? 840; + }); + + return $country ? (string) $country->id : '840'; } private function resolveCurrency(string $currency_code) { - return (string) app('currencies')->first(function($c) use ($currency_code){ + $currency = app('currencies')->first(function($c) use ($currency_code){ return $c->code == $currency_code; - }) ?? 'USD'; + }); + + return $currency ? (string) $currency->id : '1'; } public function getShipAddrCountry($data, $field) diff --git a/app/Services/Import/Quickbooks/Transfomers/InvoiceTransformer.php b/app/Services/Import/Quickbooks/Transformers/InvoiceTransformer.php similarity index 100% rename from app/Services/Import/Quickbooks/Transfomers/InvoiceTransformer.php rename to app/Services/Import/Quickbooks/Transformers/InvoiceTransformer.php diff --git a/app/Services/Import/Quickbooks/Transfomers/PaymentTransformer.php b/app/Services/Import/Quickbooks/Transformers/PaymentTransformer.php similarity index 100% rename from app/Services/Import/Quickbooks/Transfomers/PaymentTransformer.php rename to app/Services/Import/Quickbooks/Transformers/PaymentTransformer.php diff --git a/app/Services/Import/Quickbooks/Transfomers/ProductTransformer.php b/app/Services/Import/Quickbooks/Transformers/ProductTransformer.php similarity index 100% rename from app/Services/Import/Quickbooks/Transfomers/ProductTransformer.php rename to app/Services/Import/Quickbooks/Transformers/ProductTransformer.php