From 01a42bb7e2b9eb709f8809d916327dabe1c1bc0d Mon Sep 17 00:00:00 2001 From: David Bomba Date: Sun, 22 Sep 2024 19:27:34 +1000 Subject: [PATCH] QB Sync --- app/Casts/ClientSyncCast.php | 41 +++++++++ app/Casts/InvoiceSyncCast.php | 41 +++++++++ app/Casts/ProductSyncCast.php | 41 +++++++++ app/DataMapper/ClientSync.php | 35 ++++++++ app/DataMapper/InvoiceSync.php | 35 ++++++++ app/DataMapper/ProductSync.php | 35 ++++++++ app/Models/Client.php | 3 + app/Models/Invoice.php | 4 + app/Models/Product.php | 9 ++ .../Quickbooks/Jobs/QuickbooksSync.php | 89 +++++++++---------- app/Services/Quickbooks/Models/QbInvoice.php | 30 +++++++ app/Services/Quickbooks/Models/QbProduct.php | 76 ++++++++++++++++ app/Services/Quickbooks/QuickbooksService.php | 27 ++++-- .../Transformers/BaseTransformer.php | 3 +- .../Transformers/InvoiceTransformer.php | 1 + .../Transformers/ProductTransformer.php | 2 +- ...4749_2024_09_23_add_sync_column_for_qb.php | 35 ++++++++ .../Import/Quickbooks/QuickbooksTest.php | 23 ++++- 18 files changed, 472 insertions(+), 58 deletions(-) create mode 100644 app/Casts/ClientSyncCast.php create mode 100644 app/Casts/InvoiceSyncCast.php create mode 100644 app/Casts/ProductSyncCast.php create mode 100644 app/DataMapper/ClientSync.php create mode 100644 app/DataMapper/InvoiceSync.php create mode 100644 app/DataMapper/ProductSync.php create mode 100644 app/Services/Quickbooks/Models/QbInvoice.php create mode 100644 app/Services/Quickbooks/Models/QbProduct.php create mode 100644 database/migrations/2024_09_22_084749_2024_09_23_add_sync_column_for_qb.php diff --git a/app/Casts/ClientSyncCast.php b/app/Casts/ClientSyncCast.php new file mode 100644 index 000000000000..736022b3053a --- /dev/null +++ b/app/Casts/ClientSyncCast.php @@ -0,0 +1,41 @@ +qb_id = $data['qb_id']; + + return $is; + } + + public function set($model, string $key, $value, array $attributes) + { + return [ + $key => json_encode([ + 'qb_id' => $value->qb_id, + ]) + ]; + } +} diff --git a/app/Casts/InvoiceSyncCast.php b/app/Casts/InvoiceSyncCast.php new file mode 100644 index 000000000000..8776637a709e --- /dev/null +++ b/app/Casts/InvoiceSyncCast.php @@ -0,0 +1,41 @@ +qb_id = $data['qb_id']; + + return $is; + } + + public function set($model, string $key, $value, array $attributes) + { + return [ + $key => json_encode([ + 'qb_id' => $value->qb_id, + ]) + ]; + } +} diff --git a/app/Casts/ProductSyncCast.php b/app/Casts/ProductSyncCast.php new file mode 100644 index 000000000000..ca2172c13b14 --- /dev/null +++ b/app/Casts/ProductSyncCast.php @@ -0,0 +1,41 @@ +qb_id = $data['qb_id']; + + return $ps; + } + + public function set($model, string $key, $value, array $attributes) + { + return [ + $key => json_encode([ + 'qb_id' => $value->qb_id, + ]) + ]; + } +} diff --git a/app/DataMapper/ClientSync.php b/app/DataMapper/ClientSync.php new file mode 100644 index 000000000000..56b2b8e185f9 --- /dev/null +++ b/app/DataMapper/ClientSync.php @@ -0,0 +1,35 @@ + $arguments + */ + public static function castUsing(array $arguments): string + { + return ClientSyncCast::class; + } + +} diff --git a/app/DataMapper/InvoiceSync.php b/app/DataMapper/InvoiceSync.php new file mode 100644 index 000000000000..90ecde71989a --- /dev/null +++ b/app/DataMapper/InvoiceSync.php @@ -0,0 +1,35 @@ + $arguments + */ + public static function castUsing(array $arguments): string + { + return InvoiceSyncCast::class; + } + +} diff --git a/app/DataMapper/ProductSync.php b/app/DataMapper/ProductSync.php new file mode 100644 index 000000000000..1417b0b0129b --- /dev/null +++ b/app/DataMapper/ProductSync.php @@ -0,0 +1,35 @@ + $arguments + */ + public static function castUsing(array $arguments): string + { + return ProductSyncCast::class; + } + +} diff --git a/app/Models/Client.php b/app/Models/Client.php index e8ab1dfab607..25f6aa512142 100644 --- a/app/Models/Client.php +++ b/app/Models/Client.php @@ -18,6 +18,7 @@ use App\Utils\Traits\MakesDates; use App\DataMapper\FeesAndLimits; use App\Models\Traits\Excludable; use App\DataMapper\ClientSettings; +use App\DataMapper\ClientSync; use App\DataMapper\CompanySettings; use App\Services\Client\ClientService; use App\Utils\Traits\GeneratesCounter; @@ -70,6 +71,7 @@ use Illuminate\Contracts\Translation\HasLocalePreference; * @property int|null $shipping_country_id * @property object|null $settings * @property object|null $group_settings + * @property object|null $sync * @property bool $is_deleted * @property int|null $group_settings_id * @property string|null $vat_number @@ -190,6 +192,7 @@ class Client extends BaseModel implements HasLocalePreference 'last_login' => 'timestamp', 'tax_data' => 'object', 'e_invoice' => 'object', + 'sync' => ClientSync::class, ]; protected $touches = []; diff --git a/app/Models/Invoice.php b/app/Models/Invoice.php index 725b6dcd3892..750a88bd041f 100644 --- a/app/Models/Invoice.php +++ b/app/Models/Invoice.php @@ -11,6 +11,7 @@ namespace App\Models; +use App\DataMapper\InvoiceSync; use App\Utils\Ninja; use Laravel\Scout\Searchable; use Illuminate\Support\Carbon; @@ -53,6 +54,7 @@ use App\Events\Invoice\InvoiceReminderWasEmailed; * @property bool $is_deleted * @property object|array|string $line_items * @property object|null $backup + * @property object|null $sync * @property string|null $footer * @property string|null $public_notes * @property string|null $private_notes @@ -213,6 +215,8 @@ class Invoice extends BaseModel 'custom_surcharge_tax3' => 'bool', 'custom_surcharge_tax4' => 'bool', 'e_invoice' => 'object', + 'sync' => InvoiceSync::class, + ]; protected $with = []; diff --git a/app/Models/Product.php b/app/Models/Product.php index 52390a589e07..1a96b690df4e 100644 --- a/app/Models/Product.php +++ b/app/Models/Product.php @@ -11,6 +11,7 @@ namespace App\Models; +use App\DataMapper\ProductSync; use App\Utils\Traits\MakesHash; use Illuminate\Database\Eloquent\SoftDeletes; use League\CommonMark\CommonMarkConverter; @@ -43,6 +44,7 @@ use League\CommonMark\CommonMarkConverter; * @property int|null $deleted_at * @property int|null $created_at * @property int|null $updated_at + * @property object|null $sync * @property bool $is_deleted * @property float $in_stock_quantity * @property bool $stock_notification @@ -100,6 +102,13 @@ class Product extends BaseModel 'tax_id', ]; + protected $casts = [ + 'updated_at' => 'timestamp', + 'created_at' => 'timestamp', + 'deleted_at' => 'timestamp', + 'sync' => ProductSync::class, + ]; + public array $ubl_tax_map = [ self::PRODUCT_TYPE_REVERSE_TAX => 'AE', // VAT_REVERSE_CHARGE = self::PRODUCT_TYPE_EXEMPT => 'E', // EXEMPT_FROM_TAX = diff --git a/app/Services/Quickbooks/Jobs/QuickbooksSync.php b/app/Services/Quickbooks/Jobs/QuickbooksSync.php index 603b2923963f..bff3ebd4d241 100644 --- a/app/Services/Quickbooks/Jobs/QuickbooksSync.php +++ b/app/Services/Quickbooks/Jobs/QuickbooksSync.php @@ -52,12 +52,12 @@ class QuickbooksSync implements ShouldQueue 'product' => 'Item', 'client' => 'Customer', 'invoice' => 'Invoice', - 'quote' => 'Estimate', - 'purchase_order' => 'PurchaseOrder', - 'payment' => 'Payment', + // 'quote' => 'Estimate', + // 'purchase_order' => 'PurchaseOrder', + // 'payment' => 'Payment', 'sales' => 'SalesReceipt', - 'vendor' => 'Vendor', - 'expense' => 'Purchase', + // 'vendor' => 'Vendor', + // 'expense' => 'Purchase', ]; private QuickbooksService $qbs; @@ -81,8 +81,8 @@ class QuickbooksSync implements ShouldQueue $this->qbs = new QuickbooksService($this->company); $this->settings = $this->company->quickbooks->settings; - nlog("here we go!"); foreach($this->entities as $key => $entity) { + if(!$this->syncGate($key, 'pull')) { continue; } @@ -94,34 +94,49 @@ class QuickbooksSync implements ShouldQueue } } - + + /** + * Determines whether a sync is allowed based on the settings + * + * @param string $entity + * @param string $direction + * @return bool + */ private function syncGate(string $entity, string $direction): bool { return (bool) $this->settings[$entity]['sync'] && in_array($this->settings[$entity]['direction'], [$direction,'bidirectional']); } - + + /** + * Updates the gate for a given entity + * + * @param string $entity + * @return bool + */ 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) + /** + * Processes the sync for a given entity + * + * @param string $entity + * @param mixed $records + * @return void + */ + private function processEntitySync(string $entity, $records): void { match($entity){ - 'client' => $this->syncQbToNinjaClients($records), - 'product' => $this->syncQbToNinjaProducts($records), - 'invoice' => $this->syncQbToNinjaInvoices($records), - 'sales' => $this->syncQbToNinjaInvoices($records), - 'vendor' => $this->syncQbToNinjaVendors($records), - // 'quote' => $this->syncInvoices($records), - 'expense' => $this->syncQbToNinjaExpenses($records), - // 'purchase_order' => $this->syncInvoices($records), - // 'payment' => $this->syncPayment($records), + // 'client' => $this->syncQbToNinjaClients($records), + 'product' => $this->qbs->product->syncToNinja($records), + // 'invoice' => $this->syncQbToNinjaInvoices($records), + // 'sales' => $this->syncQbToNinjaInvoices($records), + // 'vendor' => $this->syncQbToNinjaVendors($records), + // 'quote' => $this->syncInvoices($records), + // 'expense' => $this->syncQbToNinjaExpenses($records), + // 'purchase_order' => $this->syncInvoices($records), + // 'payment' => $this->syncPayment($records), default => false, }; } @@ -140,6 +155,7 @@ class QuickbooksSync implements ShouldQueue nlog($ninja_invoice_data); $payment_ids = $ninja_invoice_data['payment_ids'] ?? []; + $client_id = $ninja_invoice_data['client_id'] ?? null; if(is_null($client_id)) @@ -152,7 +168,7 @@ class QuickbooksSync implements ShouldQueue $invoice->fill($ninja_invoice_data); $invoice->saveQuietly(); - $invoice = $invoice->calc()->getInvoice()->service()->markSent()->save(); + $invoice = $invoice->calc()->getInvoice()->service()->markSent()->createInvitations()->save(); foreach($payment_ids as $payment_id) { @@ -196,7 +212,8 @@ class QuickbooksSync implements ShouldQueue $search = Invoice::query() ->withTrashed() ->where('company_id', $this->company->id) - ->where('number', $ninja_invoice_data['number']); + // ->where('number', $ninja_invoice_data['number']); + ->where('sync->qb_id', $ninja_invoice_data['id']); if($search->count() == 0) { //new invoice @@ -400,27 +417,7 @@ class QuickbooksSync implements ShouldQueue return null; } - private function findProduct(string $key): ?Product - { - $search = Product::query() - ->withTrashed() - ->where('company_id', $this->company->id) - ->where('hash', $key); - - if($search->count() == 0) { - //new product - $product = ProductFactory::create($this->company->id, $this->company->owner()->id); - $product->hash = $key; - - return $product; - } elseif($search->count() == 1) { - return $this->settings['product']['update_record'] ? $search->first() : null; - } - - return null; - - - } + public function middleware() { diff --git a/app/Services/Quickbooks/Models/QbInvoice.php b/app/Services/Quickbooks/Models/QbInvoice.php new file mode 100644 index 000000000000..c4ce204676a0 --- /dev/null +++ b/app/Services/Quickbooks/Models/QbInvoice.php @@ -0,0 +1,30 @@ +service->sdk->FindById('Invoice', $id); + } + + +} diff --git a/app/Services/Quickbooks/Models/QbProduct.php b/app/Services/Quickbooks/Models/QbProduct.php new file mode 100644 index 000000000000..cc86b7c6ea77 --- /dev/null +++ b/app/Services/Quickbooks/Models/QbProduct.php @@ -0,0 +1,76 @@ +service->sdk->FindById('Item', $id); + } + + + public function syncToNinja(array $records) + { + + $product_transformer = new ProductTransformer($this->service->company); + + foreach ($records as $record) { + + $ninja_data = $product_transformer->qbToNinja($record); + + if ($product = $this->findProduct($ninja_data['id'])) { + $product->fill($ninja_data); + $product->save(); + } + } + + } + + private function findProduct(string $key): ?Product + { + $search = Product::query() + ->withTrashed() + ->where('company_id', $this->service->company->id) + ->where('sync->qb_id', $key); + + if($search->count() == 0) { + + $product = ProductFactory::create($this->service->company->id, $this->service->company->owner()->id); + + $sync = new ProductSync(); + $sync->qb_id = $key; + $product->sync = $sync; + + return $product; + + } elseif($search->count() == 1) { + return $this->service->settings['product']['update_record'] ? $search->first() : null; + } + + return null; + + + } +} diff --git a/app/Services/Quickbooks/QuickbooksService.php b/app/Services/Quickbooks/QuickbooksService.php index 9d5b45fdd18f..a281d2346f41 100644 --- a/app/Services/Quickbooks/QuickbooksService.php +++ b/app/Services/Quickbooks/QuickbooksService.php @@ -11,16 +11,19 @@ namespace App\Services\Quickbooks; -use App\Factory\ClientContactFactory; -use App\Factory\ClientFactory; -use App\Factory\InvoiceFactory; -use App\Factory\ProductFactory; use App\Models\Client; use App\Models\Company; use App\Models\Invoice; use App\Models\Product; -use App\Services\Quickbooks\Jobs\QuickbooksSync; +use App\Factory\ClientFactory; +use App\Factory\InvoiceFactory; +use App\Factory\ProductFactory; +use App\Factory\ClientContactFactory; +use App\DataMapper\QuickbooksSettings; use QuickBooksOnline\API\Core\CoreConstants; +use App\Services\Quickbooks\Models\QbInvoice; +use App\Services\Quickbooks\Models\QbProduct; +use App\Services\Quickbooks\Jobs\QuickbooksSync; use QuickBooksOnline\API\DataService\DataService; use App\Services\Quickbooks\Transformers\ClientTransformer; use App\Services\Quickbooks\Transformers\InvoiceTransformer; @@ -31,9 +34,15 @@ class QuickbooksService { public DataService $sdk; + public QbInvoice $invoice; + + public QbProduct $product; + + public array $settings; + private bool $testMode = true; - public function __construct(private Company $company) + public function __construct(public Company $company) { $this->init(); } @@ -61,6 +70,12 @@ class QuickbooksService $this->sdk->setMinorVersion("73"); $this->sdk->throwExceptionOnError(true); + $this->invoice = new QbInvoice($this); + + $this->product = new QbProduct($this); + + $this->settings = $this->company->quickbooks->settings; + return $this; } diff --git a/app/Services/Quickbooks/Transformers/BaseTransformer.php b/app/Services/Quickbooks/Transformers/BaseTransformer.php index 66f3ff45195f..bc5639438f41 100644 --- a/app/Services/Quickbooks/Transformers/BaseTransformer.php +++ b/app/Services/Quickbooks/Transformers/BaseTransformer.php @@ -66,7 +66,8 @@ class BaseTransformer $client = Client::query() ->withTrashed() ->where('company_id', $this->company->id) - ->where('number', $customer_reference_id) + // ->where('number', $customer_reference_id) + ->where('sync->qb_id', $customer_reference_id) ->first(); return $client ? $client->id : null; diff --git a/app/Services/Quickbooks/Transformers/InvoiceTransformer.php b/app/Services/Quickbooks/Transformers/InvoiceTransformer.php index 1ba7cedbfaf4..d2c85a31b005 100644 --- a/app/Services/Quickbooks/Transformers/InvoiceTransformer.php +++ b/app/Services/Quickbooks/Transformers/InvoiceTransformer.php @@ -38,6 +38,7 @@ class InvoiceTransformer extends BaseTransformer $client_id = $this->getClientId(data_get($qb_data, 'CustomerRef.value', null)); return $client_id ? [ + 'id' => data_get($qb_data, 'Id.value', false), 'client_id' => $client_id, 'number' => data_get($qb_data, 'DocNumber', false), 'date' => data_get($qb_data, 'TxnDate', now()->format('Y-m-d')), diff --git a/app/Services/Quickbooks/Transformers/ProductTransformer.php b/app/Services/Quickbooks/Transformers/ProductTransformer.php index 7b638d23e559..5cab5b45f1a1 100644 --- a/app/Services/Quickbooks/Transformers/ProductTransformer.php +++ b/app/Services/Quickbooks/Transformers/ProductTransformer.php @@ -33,7 +33,7 @@ class ProductTransformer extends BaseTransformer nlog(data_get($data, 'Id', null)); return [ - 'hash' => data_get($data, 'Id.value', null), + 'id' => data_get($data, 'Id.value', null), 'product_key' => data_get($data, 'Name', data_get($data, 'FullyQualifiedName','')), 'notes' => data_get($data, 'Description', ''), 'cost' => data_get($data, 'PurchaseCost', 0), diff --git a/database/migrations/2024_09_22_084749_2024_09_23_add_sync_column_for_qb.php b/database/migrations/2024_09_22_084749_2024_09_23_add_sync_column_for_qb.php new file mode 100644 index 000000000000..a863a578b83d --- /dev/null +++ b/database/migrations/2024_09_22_084749_2024_09_23_add_sync_column_for_qb.php @@ -0,0 +1,35 @@ +text('sync')->nullable(); + }); + + Schema::table('invoices', function (Blueprint $table) { + $table->text('sync')->nullable(); + }); + + Schema::table('products', function (Blueprint $table) { + $table->text('sync')->nullable(); + }); + + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + // + } +}; diff --git a/tests/Feature/Import/Quickbooks/QuickbooksTest.php b/tests/Feature/Import/Quickbooks/QuickbooksTest.php index ea9a76b0fbf1..5fa4862bcde0 100644 --- a/tests/Feature/Import/Quickbooks/QuickbooksTest.php +++ b/tests/Feature/Import/Quickbooks/QuickbooksTest.php @@ -12,11 +12,14 @@ use Tests\MockAccountData; use Illuminate\Support\Facades\Cache; use Mockery; use App\Models\Client; +use App\Models\Company; use App\Models\Product; use App\Models\Invoice; +use App\Services\Quickbooks\QuickbooksService; use Illuminate\Support\Str; use ReflectionClass; use Illuminate\Support\Facades\Auth; +use QuickBooksOnline\API\Facades\Invoice as QbInvoice; class QuickbooksTest extends TestCase { @@ -29,13 +32,25 @@ class QuickbooksTest extends TestCase protected function setUp(): void { - parent::setUp(); - $this->markTestSkipped('no bueno'); + parent::setUp(); + if(config('ninja.is_travis')) + { + $this->markTestSkipped('No need to run this test on Travis'); + } + elseif(Company::whereNotNull('quickbooks')->count() == 0){ + $this->markTestSkipped('No need to run this test on Travis'); + } } - public function testCustomerSync() + public function testCreateInvoiceInQb() { - $data = (json_decode(file_get_contents(base_path('tests/Feature/Import/Quickbooks/customers.json')), false)); + + $c = Company::whereNotNull('quickbooks')->first(); + + $qb = new QuickbooksService($c); + + + } }