mirror of
https://github.com/invoiceninja/invoiceninja.git
synced 2025-07-09 03:14:30 -04:00
Refactor for QB sync
This commit is contained in:
parent
8fadba5545
commit
cb7c87e053
@ -11,32 +11,20 @@
|
||||
|
||||
namespace App\DataMapper;
|
||||
|
||||
enum SyncDirection: string
|
||||
{
|
||||
case PUSH = 'push';
|
||||
case PULL = 'pull';
|
||||
case BIDIRECTIONAL = 'bidirectional';
|
||||
}
|
||||
use App\Enum\SyncDirection;
|
||||
|
||||
/**
|
||||
* QuickbooksSyncMap.
|
||||
*/
|
||||
class QuickbooksSyncMap
|
||||
{
|
||||
public bool $sync = true;
|
||||
|
||||
public bool $update_record = true;
|
||||
|
||||
public SyncDirection $direction = SyncDirection::BIDIRECTIONAL;
|
||||
public SyncDirection $direction = SyncDirection::BIDIRECTIONAL;
|
||||
|
||||
public function __construct(array $attributes = [])
|
||||
{
|
||||
$this->sync = $attributes['sync'] ?? true;
|
||||
$this->update_record = $attributes['update_record'] ?? true;
|
||||
$this->direction = isset($attributes['direction'])
|
||||
? SyncDirection::from($attributes['direction'])
|
||||
: SyncDirection::BIDIRECTIONAL;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
19
app/Enum/SyncDirection.php
Normal file
19
app/Enum/SyncDirection.php
Normal file
@ -0,0 +1,19 @@
|
||||
<?php
|
||||
/**
|
||||
* Invoice Ninja (https://invoiceninja.com).
|
||||
*
|
||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||
*
|
||||
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
*
|
||||
* @license https://www.elastic.co/licensing/elastic-license
|
||||
*/
|
||||
|
||||
namespace App\Enum;
|
||||
|
||||
enum SyncDirection: string
|
||||
{
|
||||
case PUSH = 'push'; // only creates and updates records created by Invoice Ninja.
|
||||
case PULL = 'pull'; // creates and updates record from QB.
|
||||
case BIDIRECTIONAL = 'bidirectional'; // creates and updates records created by Invoice Ninja and from QB.
|
||||
}
|
@ -28,7 +28,7 @@ class SearchController extends Controller
|
||||
|
||||
public function __invoke(GenericSearchRequest $request)
|
||||
{
|
||||
if(Ninja::isHosted() && $request->has('search') && $request->input('search') !== '') {
|
||||
if(config('scount.driver') == 'elastic' && $request->has('search') && $request->input('search') !== '') {
|
||||
try{
|
||||
return $this->search($request->input('search', ''));
|
||||
} catch(\Exception $e) {
|
||||
|
@ -84,7 +84,7 @@ class QuickbooksImport implements ShouldQueue
|
||||
|
||||
foreach($this->entities as $key => $entity) {
|
||||
|
||||
if(!$this->qbs->syncGate($key, 'pull')) {
|
||||
if(!$this->qbs->syncable($key, \App\Enum\SyncDirection::PULL)) {
|
||||
nlog('skipping ' . $key);
|
||||
continue;
|
||||
}
|
||||
@ -153,7 +153,7 @@ class QuickbooksImport implements ShouldQueue
|
||||
$contact->fill($ninja_data[1]);
|
||||
$contact->saveQuietly();
|
||||
}
|
||||
elseif($this->qbs->updateGate('vendor')){
|
||||
elseif($this->qbs->syncable('vendor', (\App\Enum\SyncDirection::PULL)->value)){
|
||||
$contact->fill($ninja_data[1]);
|
||||
$contact->saveQuietly();
|
||||
}
|
||||
@ -194,7 +194,7 @@ class QuickbooksImport implements ShouldQueue
|
||||
return ExpenseFactory::create($this->company->id, $this->company->owner()->id);
|
||||
}
|
||||
elseif($search->count() == 1) {
|
||||
return $this->settings->expense->update_record ? $search->first() : null;
|
||||
return $this->service->syncable('expense', \App\Enum\SyncDirection::PULL) ? $search->first() : null;
|
||||
}
|
||||
|
||||
return null;
|
||||
@ -224,7 +224,8 @@ class QuickbooksImport implements ShouldQueue
|
||||
return VendorFactory::create($this->company->id, $this->company->owner()->id);
|
||||
}
|
||||
elseif($search->count() == 1) {
|
||||
return $this->settings->vendor->update_record ? $search->first() : null;
|
||||
|
||||
return $this->service->syncable('vendor', \App\Enum\SyncDirection::PULL) ? $search->first() : null;
|
||||
}
|
||||
|
||||
return null;
|
||||
@ -258,7 +259,8 @@ class QuickbooksImport implements ShouldQueue
|
||||
return $client;
|
||||
}
|
||||
elseif($search->count() == 1) {
|
||||
return $this->settings->client->update_record ? $search->first() : null;
|
||||
|
||||
return $this->service->syncable('client', \App\Enum\SyncDirection::PULL) ? $search->first() : null;
|
||||
}
|
||||
|
||||
return null;
|
||||
|
@ -69,7 +69,7 @@ class QbClient implements SyncInterface
|
||||
$contact->fill($ninja_data[1]);
|
||||
$contact->saveQuietly();
|
||||
}
|
||||
elseif($this->service->updateGate('client')){
|
||||
elseif($this->service->syncable('client', \App\Enum\SyncDirection::PULL)){
|
||||
$contact->fill($ninja_data[1]);
|
||||
$contact->saveQuietly();
|
||||
}
|
||||
@ -101,7 +101,8 @@ class QbClient implements SyncInterface
|
||||
return $client;
|
||||
|
||||
} elseif ($search->count() == 1) {
|
||||
return $this->service->settings->client->update_record ? $search->first() : null;
|
||||
|
||||
return $this->service->syncable('client', \App\Enum\SyncDirection::PULL) ? $search->first() : null;
|
||||
}
|
||||
|
||||
return null;
|
||||
|
@ -43,58 +43,7 @@ class QbInvoice implements SyncInterface
|
||||
|
||||
foreach ($records as $record) {
|
||||
|
||||
$ninja_invoice_data = $this->invoice_transformer->qbToNinja($record);
|
||||
|
||||
$payment_ids = $ninja_invoice_data['payment_ids'] ?? [];
|
||||
|
||||
$client_id = $ninja_invoice_data['client_id'] ?? null;
|
||||
|
||||
if (is_null($client_id)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
unset($ninja_invoice_data['payment_ids']);
|
||||
|
||||
if ($invoice = $this->findInvoice($ninja_invoice_data['id'], $ninja_invoice_data['client_id'])) {
|
||||
|
||||
if($invoice->id)
|
||||
$this->processQbToNinjaInvoiceUpdate($ninja_invoice_data, $invoice);
|
||||
|
||||
$invoice->fill($ninja_invoice_data);
|
||||
$invoice->saveQuietly();
|
||||
|
||||
$invoice = $invoice->calc()->getInvoice()->service()->markSent()->createInvitations()->save();
|
||||
|
||||
foreach ($payment_ids as $payment_id) {
|
||||
|
||||
$payment = $this->service->sdk->FindById('Payment', $payment_id);
|
||||
|
||||
$payment_transformer = new PaymentTransformer($this->service->company);
|
||||
|
||||
$transformed = $payment_transformer->qbToNinja($payment);
|
||||
|
||||
$ninja_payment = $payment_transformer->buildPayment($payment);
|
||||
$ninja_payment->service()->applyNumber()->save();
|
||||
|
||||
$paymentable = new \App\Models\Paymentable();
|
||||
$paymentable->payment_id = $ninja_payment->id;
|
||||
$paymentable->paymentable_id = $invoice->id;
|
||||
$paymentable->paymentable_type = 'invoices';
|
||||
$paymentable->amount = $transformed['applied'] + $ninja_payment->credits->sum('amount');
|
||||
$paymentable->created_at = $ninja_payment->date; //@phpstan-ignore-line
|
||||
$paymentable->save();
|
||||
|
||||
$invoice->service()->applyPayment($ninja_payment, $paymentable->amount);
|
||||
|
||||
}
|
||||
|
||||
if ($record instanceof IPPSalesReceipt) {
|
||||
$invoice->service()->markPaid()->save();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
$ninja_invoice_data = false;
|
||||
$this->syncNinjaInvoice($record);
|
||||
|
||||
}
|
||||
|
||||
@ -105,7 +54,7 @@ class QbInvoice implements SyncInterface
|
||||
|
||||
}
|
||||
|
||||
private function processQbToNinjaInvoiceUpdate(array $ninja_invoice_data, Invoice $invoice): void
|
||||
private function qbInvoiceUpdate(array $ninja_invoice_data, Invoice $invoice): void
|
||||
{
|
||||
$current_ninja_invoice_balance = $invoice->balance;
|
||||
$qb_invoice_balance = $ninja_invoice_data['balance'];
|
||||
@ -130,7 +79,7 @@ class QbInvoice implements SyncInterface
|
||||
->where('company_id', $this->service->company->id)
|
||||
->where('sync->qb_id', $id);
|
||||
|
||||
if($search->count() == 0 && $client_id) {
|
||||
if($search->count() == 0) {
|
||||
$invoice = InvoiceFactory::create($this->service->company->id, $this->service->company->owner()->id);
|
||||
$invoice->client_id = $client_id;
|
||||
|
||||
@ -140,32 +89,125 @@ class QbInvoice implements SyncInterface
|
||||
|
||||
return $invoice;
|
||||
} elseif($search->count() == 1) {
|
||||
return $this->service->settings->invoice->update_record ? $search->first() : null;
|
||||
|
||||
return $this->service->syncable('invoice', \App\Enum\SyncDirection::PULL) ? $search->first() : null;
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
}
|
||||
|
||||
|
||||
public function sync(string $id, string $last_updated): void
|
||||
public function sync($id, string $last_updated): void
|
||||
{
|
||||
|
||||
$qb_record = $this->find($id);
|
||||
|
||||
if($this->service->updateGate('invoice') && $invoice = $this->findInvoice($id))
|
||||
if($this->service->syncable('invoice', \App\Enum\SyncDirection::PULL))
|
||||
{
|
||||
|
||||
//logic here to determine if we should update the record
|
||||
if(Carbon::parse($last_updated)->gt(Carbon::parse($invoice->updated_at)))
|
||||
$invoice = $this->findInvoice($id);
|
||||
|
||||
if(data_get($qb_record, 'TxnStatus') === 'Voided')
|
||||
{
|
||||
$this->delete($id);
|
||||
return;
|
||||
}
|
||||
|
||||
if(!$invoice->id){
|
||||
$this->syncNinjaInvoice($qb_record);
|
||||
}
|
||||
elseif(Carbon::parse($last_updated)->gt(Carbon::parse($invoice->updated_at)))
|
||||
{
|
||||
$ninja_invoice_data = $this->invoice_transformer->qbToNinja($qb_record);
|
||||
$this->invoice_repository->save($ninja_invoice_data, $invoice);
|
||||
|
||||
}
|
||||
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* syncNinjaInvoice
|
||||
*
|
||||
* @param $record
|
||||
* @return void
|
||||
*/
|
||||
public function syncNinjaInvoice($record): void
|
||||
{
|
||||
|
||||
$ninja_invoice_data = $this->invoice_transformer->qbToNinja($record);
|
||||
|
||||
$payment_ids = $ninja_invoice_data['payment_ids'] ?? [];
|
||||
|
||||
$client_id = $ninja_invoice_data['client_id'] ?? null;
|
||||
|
||||
if (is_null($client_id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
unset($ninja_invoice_data['payment_ids']);
|
||||
|
||||
if ($invoice = $this->findInvoice($ninja_invoice_data['id'], $ninja_invoice_data['client_id'])) {
|
||||
|
||||
if ($invoice->id) {
|
||||
$this->qbInvoiceUpdate($ninja_invoice_data, $invoice);
|
||||
}
|
||||
|
||||
//new invoice scaffold
|
||||
$invoice->fill($ninja_invoice_data);
|
||||
$invoice->saveQuietly();
|
||||
|
||||
$invoice = $invoice->calc()->getInvoice()->service()->markSent()->applyNumber()->createInvitations()->save();
|
||||
|
||||
foreach ($payment_ids as $payment_id) {
|
||||
|
||||
$payment = $this->service->sdk->FindById('Payment', $payment_id);
|
||||
|
||||
$payment_transformer = new PaymentTransformer($this->service->company);
|
||||
|
||||
$transformed = $payment_transformer->qbToNinja($payment);
|
||||
|
||||
$ninja_payment = $payment_transformer->buildPayment($payment);
|
||||
$ninja_payment->service()->applyNumber()->save();
|
||||
|
||||
$paymentable = new \App\Models\Paymentable();
|
||||
$paymentable->payment_id = $ninja_payment->id;
|
||||
$paymentable->paymentable_id = $invoice->id;
|
||||
$paymentable->paymentable_type = 'invoices';
|
||||
$paymentable->amount = $transformed['applied'] + $ninja_payment->credits->sum('amount');
|
||||
$paymentable->created_at = $ninja_payment->date; //@phpstan-ignore-line
|
||||
$paymentable->save();
|
||||
|
||||
$invoice->service()->applyPayment($ninja_payment, $paymentable->amount);
|
||||
|
||||
}
|
||||
|
||||
if ($record instanceof IPPSalesReceipt) {
|
||||
$invoice->service()->markPaid()->save();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
$ninja_invoice_data = false;
|
||||
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the invoice from Ninja and sets the sync to null
|
||||
*
|
||||
* @param string $id
|
||||
* @return void
|
||||
*/
|
||||
public function delete($id): void
|
||||
{
|
||||
$qb_record = $this->find($id);
|
||||
|
||||
if($this->service->syncable('invoice', \App\Enum\SyncDirection::PULL) && $invoice = $this->findInvoice($id))
|
||||
{
|
||||
$invoice->sync = null;
|
||||
$invoice->saveQuietly();
|
||||
$this->invoice_repository->delete($invoice);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -73,7 +73,8 @@ class QbProduct implements SyncInterface
|
||||
return $product;
|
||||
|
||||
} elseif($search->count() == 1) {
|
||||
return $this->service->settings->product->update_record ? $search->first() : null;
|
||||
|
||||
return $this->service->syncable('product', \App\Enum\SyncDirection::PULL) ? $search->first() : null;
|
||||
}
|
||||
|
||||
return null;
|
||||
@ -84,7 +85,7 @@ class QbProduct implements SyncInterface
|
||||
{
|
||||
$qb_record = $this->find($id);
|
||||
|
||||
if($this->service->updateGate('product') && $ninja_record = $this->findProduct($id))
|
||||
if($this->service->syncable('product', \App\Enum\SyncDirection::PULL) && $ninja_record = $this->findProduct($id))
|
||||
{
|
||||
|
||||
if(Carbon::parse($last_updated) > Carbon::parse($ninja_record->updated_at))
|
||||
|
@ -139,31 +139,17 @@ class QuickbooksService
|
||||
{
|
||||
return $this->sdk->FindById($entity, $id);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Updates the gate for a given entity
|
||||
* Flag to determine if a sync is allowed in either direction
|
||||
*
|
||||
* @param string $entity
|
||||
* @param mixed $direction
|
||||
* @return bool
|
||||
*/
|
||||
public function updateGate(string $entity): bool
|
||||
public function syncable(string $entity, \App\Enum\SyncDirection $direction): bool
|
||||
{
|
||||
nlog($this->settings->{$entity}->sync);
|
||||
nlog($this->settings->{$entity}->update_record);
|
||||
|
||||
return $this->settings->{$entity}->sync && $this->settings->{$entity}->update_record;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether a sync is allowed based on the settings
|
||||
*
|
||||
* @param string $entity
|
||||
* @param string $direction
|
||||
* @return bool
|
||||
*/
|
||||
public function syncGate(string $entity, string $direction): bool
|
||||
{
|
||||
return (bool) $this->settings->{$entity}->sync && in_array($this->settings->{$entity}->direction->value, [$direction, 'bidirectional']);
|
||||
return $this->settings->{$entity}->direction === $direction || $this->settings->{$entity}->direction === \App\Enum\SyncDirection::BIDIRECTIONAL;
|
||||
}
|
||||
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user