Merge pull request #9948 from turbo124/v5-develop

Minor fixes.
This commit is contained in:
David Bomba 2024-08-27 09:42:24 +10:00 committed by GitHub
commit df914450e2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
36 changed files with 12832 additions and 760 deletions

View File

@ -0,0 +1,50 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Casts;
use App\DataMapper\QuickbooksSettings;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
class QuickbooksSettingsCast implements CastsAttributes
{
public function get($model, string $key, $value, array $attributes)
{
$data = json_decode($value, true);
if(!is_array($data))
return null;
$qb = new QuickbooksSettings();
$qb->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,
])
];
}
}

View File

@ -0,0 +1,59 @@
<?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\DataMapper;
use Illuminate\Contracts\Database\Eloquent\Castable;
use App\Casts\QuickbooksSettingsCast;
/**
* QuickbooksSettings.
*/
class QuickbooksSettings implements Castable
{
public string $accessTokenKey;
public string $refresh_token;
public string $realmID;
public int $accessTokenExpiresAt;
public int $refreshTokenExpiresAt;
/**
* entity client,invoice,quote,purchase_order,vendor,payment
* sync true/false
* update_record true/false
* direction push/pull/birectional
* */
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<string, mixed> $arguments
*/
public static function castUsing(array $arguments): string
{
return QuickbooksSettingsCast::class;
}
}

View File

@ -99,7 +99,7 @@ class TransactionTransformer implements BankRevenueInterface
} elseif (array_key_exists('internalTransactionId', $transaction)) {
$transactionId = $transaction["internalTransactionId"];
} else {
nlog(`Invalid Input for nordigen transaction transformer: ` . $transaction);
nlog('Invalid Input for nordigen transaction transformer: ' . $transaction);
throw new \Exception('invalid dataset: missing transactionId - Please report this error to the developer');
}

View File

@ -15,7 +15,7 @@ use App\Http\Requests\Quickbooks\AuthorizedQuickbooksRequest;
use App\Libraries\MultiDB;
use Illuminate\Support\Facades\Cache;
use App\Http\Requests\Quickbooks\AuthQuickbooksRequest;
use App\Services\Import\Quickbooks\QuickbooksService;
use App\Services\Quickbooks\QuickbooksService;
class ImportQuickbooksController extends BaseController
{

View File

@ -93,6 +93,12 @@ class StoreInvoiceRequest extends Request
/** @var \App\Models\User $user */
$user = auth()->user();
if(\Illuminate\Support\Facades\Cache::has($this->ip()."|INVOICE|".$this->input('client_id', '')."|".$user->company()->company_key)) {
usleep(200000);
}
\Illuminate\Support\Facades\Cache::put($this->ip()."|INVOICE|".$this->input('client_id', '')."|".$user->company()->company_key,1);
$input = $this->all();
$input = $this->decodePrimaryKeys($input);

View File

@ -1,99 +0,0 @@
<?php
/**
* Invoice Ninja (https://clientninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Import\Transformer\Quickbooks;
use App\Import\Transformer\Quickbooks\CommonTrait;
use App\Import\Transformer\BaseTransformer;
use App\Models\Client as Model;
use App\Models\ClientContact;
use App\Import\ImportException;
use Illuminate\Support\Str;
/**
* Class ClientTransformer.
*/
class ClientTransformer extends BaseTransformer
{
use CommonTrait {
transform as preTransform;
}
private $fillable = [
'name' => 'CompanyName',
'phone' => 'PrimaryPhone.FreeFormNumber',
'country_id' => 'BillAddr.Country',
'state' => 'BillAddr.CountrySubDivisionCode',
'address1' => 'BillAddr.Line1',
'city' => 'BillAddr.City',
'postal_code' => 'BillAddr.PostalCode',
'shipping_country_id' => 'ShipAddr.Country',
'shipping_state' => 'ShipAddr.CountrySubDivisionCode',
'shipping_address1' => 'ShipAddr.Line1',
'shipping_city' => 'ShipAddr.City',
'shipping_postal_code' => 'ShipAddr.PostalCode',
'public_notes' => 'Notes'
];
public function __construct($company)
{
parent::__construct($company);
$this->model = new Model();
}
/**
* Transforms a Customer array into a ClientContact model.
*
* @param array $data
* @return array|bool
*/
public function transform($data)
{
$transformed_data = [];
// Assuming 'customer_name' is equivalent to 'CompanyName'
if (isset($data['CompanyName']) && $this->hasClient($data['CompanyName'])) {
return false;
}
$transformed_data = $this->preTransform($data);
$transformed_data['contacts'][0] = $this->getContacts($data)->toArray() + ['company_id' => $this->company->id, 'user_id' => $this->company->owner()->id ];
return $transformed_data;
}
protected function getContacts($data)
{
return (new ClientContact())->fill([
'first_name' => $this->getString($data, 'GivenName'),
'last_name' => $this->getString($data, 'FamilyName'),
'phone' => $this->getString($data, 'PrimaryPhone.FreeFormNumber'),
'email' => $this->getString($data, 'PrimaryEmailAddr.Address'),
'company_id' => $this->company->id,
'user_id' => $this->company->owner()->id,
'send_email' => true,
]);
}
public function getShipAddrCountry($data, $field)
{
return is_null(($c = $this->getString($data, $field))) ? null : $this->getCountryId($c);
}
public function getBillAddrCountry($data, $field)
{
return is_null(($c = $this->getString($data, $field))) ? null : $this->getCountryId($c);
}
}

View File

@ -1,37 +0,0 @@
<?php
namespace App\Import\Transformer\Quickbooks;
use Illuminate\Support\Arr;
trait CommonTrait
{
protected $model;
public function getString($data, $field)
{
return Arr::get($data, $field);
}
public function getCreateTime($data, $field = null)
{
return $this->parseDateOrNull($data, 'MetaData.CreateTime');
}
public function getLastUpdatedTime($data, $field = null)
{
return $this->parseDateOrNull($data, 'MetaData.LastUpdatedTime');
}
public function transform($data)
{
$transformed = [];
foreach ($this->fillable as $key => $field) {
$transformed[$key] = is_null((($v = $this->getString($data, $field)))) ? null : (method_exists($this, ($method = "get{$field}")) ? call_user_func([$this, $method], $data, $field) : $this->getString($data, $field));
}
return $this->model->fillable(array_keys($this->fillable))->fill($transformed)->toArray() + ['company_id' => $this->company->id ] ;
}
}

View File

@ -1,200 +0,0 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Import\Transformer\Quickbooks;
use App\Models\Invoice;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
use App\DataMapper\InvoiceItem;
use App\Import\ImportException;
use App\Models\Invoice as Model;
use App\Import\Transformer\BaseTransformer;
use App\Import\Transformer\Quickbooks\CommonTrait;
use App\Import\Transformer\Quickbooks\ClientTransformer;
/**
* Class InvoiceTransformer.
*/
class InvoiceTransformer extends BaseTransformer
{
use CommonTrait {
transform as preTransform;
}
private $fillable = [
'amount' => "TotalAmt",
'line_items' => "Line",
'due_date' => "DueDate",
'partial' => "Deposit",
'balance' => "Balance",
'private_notes' => "CustomerMemo",
'public_notes' => "CustomerMemo",
'number' => "DocNumber",
'created_at' => "CreateTime",
'updated_at' => "LastUpdatedTime",
'payments' => 'LinkedTxn',
'status_id' => 'InvoiceStatus',
];
public function __construct($company)
{
parent::__construct($company);
$this->model = new Model();
}
public function getInvoiceStatus($data)
{
return Invoice::STATUS_SENT;
}
public function transform($data)
{
return $this->preTransform($data) + $this->getInvoiceClient($data);
}
public function getTotalAmt($data)
{
return (float) $this->getString($data, 'TotalAmt');
}
public function getLine($data)
{
return array_map(function ($item) {
return [
'description' => $this->getString($item, 'Description'),
'product_key' => $this->getString($item, 'Description'),
'quantity' => (int) $this->getString($item, 'SalesItemLineDetail.Qty'),
'unit_price' => (float) $this->getString($item, 'SalesItemLineDetail.UnitPrice'),
'line_total' => (float) $this->getString($item, 'Amount'),
'cost' => (float) $this->getString($item, 'SalesItemLineDetail.UnitPrice'),
'product_cost' => (float) $this->getString($item, 'SalesItemLineDetail.UnitPrice'),
'tax_amount' => (float) $this->getString($item, 'TxnTaxDetail.TotalTax'),
];
}, array_filter($this->getString($data, 'Line'), function ($item) {
return $this->getString($item, 'DetailType') !== 'SubTotalLineDetail';
}));
}
public function getInvoiceClient($data, $field = null)
{
/**
* "CustomerRef": {
"value": "23",
"name": ""Barnett Design
},
"CustomerMemo": {
"value": "Thank you for your business and have a great day!"
},
"BillAddr": {
"Id": "58",
"Line1": "Shara Barnett",
"Line2": "Barnett Design",
"Line3": "19 Main St.",
"Line4": "Middlefield, CA 94303",
"Lat": "37.4530553",
"Long": "-122.1178261"
},
"ShipAddr": {
"Id": "24",
"Line1": "19 Main St.",
"City": "Middlefield",
"CountrySubDivisionCode": "CA",
"PostalCode": "94303",
"Lat": "37.445013",
"Long": "-122.1391443"
},"BillEmail": {
"Address": "Design@intuit.com"
},
[
'name' => 'CompanyName',
'phone' => 'PrimaryPhone.FreeFormNumber',
'country_id' => 'BillAddr.Country',
'state' => 'BillAddr.CountrySubDivisionCode',
'address1' => 'BillAddr.Line1',
'city' => 'BillAddr.City',
'postal_code' => 'BillAddr.PostalCode',
'shipping_country_id' => 'ShipAddr.Country',
'shipping_state' => 'ShipAddr.CountrySubDivisionCode',
'shipping_address1' => 'ShipAddr.Line1',
'shipping_city' => 'ShipAddr.City',
'shipping_postal_code' => 'ShipAddr.PostalCode',
'public_notes' => 'Notes'
];
*/
$bill_address = (object) $this->getString($data, 'BillAddr');
$ship_address = $this->getString($data, 'ShipAddr');
$customer = explode(" ", $this->getString($data, 'CustomerRef.name'));
$customer = ['GivenName' => $customer[0], 'FamilyName' => $customer[1]];
$has_company = property_exists($bill_address, 'Line4');
$address = $has_company ? $bill_address->Line4 : $bill_address->Line3;
$address_1 = substr($address, 0, stripos($address, ','));
$address = array_filter([$address_1] + (explode(' ', substr($address, stripos($address, ",") + 1))));
$client_id = null;
$client =
[
"CompanyName" => $has_company ? $bill_address->Line2 : $bill_address->Line1,
"BillAddr" => array_combine(['City','CountrySubDivisionCode','PostalCode'], array_pad($address, 3, 'N/A')) + ['Line1' => $has_company ? $bill_address->Line3 : $bill_address->Line2 ],
"ShipAddr" => $ship_address
] + $customer + ['PrimaryEmailAddr' => ['Address' => $this->getString($data, 'BillEmail.Address') ]];
if($this->hasClient($client['CompanyName'])) {
$client_id = $this->getClient($client['CompanyName'], $this->getString($client, 'PrimaryEmailAddr.Address'));
}
return ['client' => (new ClientTransformer($this->company))->transform($client), 'client_id' => $client_id ];
}
public function getDueDate($data)
{
return $this->parseDateOrNull($data, 'DueDate');
}
public function getDeposit($data)
{
return (float) $this->getString($data, 'Deposit');
}
public function getBalance($data)
{
return (float) $this->getString($data, 'Balance');
}
public function getCustomerMemo($data)
{
return $this->getString($data, 'CustomerMemo.value');
}
public function getDocNumber($data, $field = null)
{
return sprintf(
"%s-%s",
$this->getString($data, 'DocNumber'),
$this->getString($data, 'Id.value')
);
}
public function getLinkedTxn($data)
{
$payments = $this->getString($data, 'LinkedTxn');
if(empty($payments)) {
return [];
}
return [[
'amount' => $this->getTotalAmt($data),
'date' => $this->parseDateOrNull($data, 'TxnDate')
]];
}
}

View File

@ -1,106 +0,0 @@
<?php
/**
* Invoice Ninja (https://Paymentninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Import\Transformer\Quickbooks;
use App\Import\Transformer\Quickbooks\CommonTrait;
use App\Import\Transformer\BaseTransformer;
use App\Models\Payment as Model;
use App\Import\ImportException;
use Illuminate\Support\Str;
use Illuminate\Support\Arr;
use App\Models\Invoice;
/**
*
* Class PaymentTransformer.
*/
class PaymentTransformer extends BaseTransformer
{
use CommonTrait;
protected $fillable = [
'number' => "PaymentRefNum",
'amount' => "TotalAmt",
"client_id" => "CustomerRef",
"currency_id" => "CurrencyRef",
'date' => "TxnDate",
"invoices" => "Line",
'private_notes' => "PrivateNote",
'created_at' => "CreateTime",
'updated_at' => "LastUpdatedTime"
];
public function __construct($company)
{
parent::__construct($company);
$this->model = new Model();
}
public function getTotalAmt($data, $field = null)
{
return (float) $this->getString($data, $field);
}
public function getTxnDate($data, $field = null)
{
return $this->parseDateOrNull($data, $field);
}
public function getCustomerRef($data, $field = null)
{
return $this->getClient($this->getString($data, 'CustomerRef.name'), null);
}
public function getCurrencyRef($data, $field = null)
{
return $this->getCurrencyByCode($data['CurrencyRef'], 'value');
}
public function getLine($data, $field = null)
{
$invoices = [];
$invoice = $this->getString($data, 'Line.LinkedTxn.TxnType');
if(is_null($invoice) || $invoice !== 'Invoice') {
return $invoices;
}
if(is_null(($invoice_id = $this->getInvoiceId($this->getString($data, 'Line.LinkedTxn.TxnId.value'))))) {
return $invoices;
}
return [[
'amount' => (float) $this->getString($data, 'Line.Amount'),
'invoice_id' => $invoice_id
]];
}
/**
* @param $invoice_number
*
* @return int|null
*/
public function getInvoiceId($invoice_number)
{
$invoice = Invoice::query()->where('company_id', $this->company->id)
->where('is_deleted', false)
->where(
"number",
"LIKE",
"%-$invoice_number%",
)
->first();
return $invoice ? $invoice->id : null;
}
}

View File

@ -1,61 +0,0 @@
<?php
/**
* Invoice Ninja (https://Productninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Import\Transformer\Quickbooks;
use App\Import\Transformer\Quickbooks\CommonTrait;
use App\Import\Transformer\BaseTransformer;
use App\Models\Product as Model;
use App\Import\ImportException;
/**
* Class ProductTransformer.
*/
class ProductTransformer extends BaseTransformer
{
use CommonTrait;
protected $fillable = [
'product_key' => 'Name',
'notes' => 'Description',
'cost' => 'PurchaseCost',
'price' => 'UnitPrice',
'quantity' => 'QtyOnHand',
'in_stock_quantity' => 'QtyOnHand',
'created_at' => 'CreateTime',
'updated_at' => 'LastUpdatedTime',
];
public function __construct($company)
{
parent::__construct($company);
$this->model = new Model();
}
public function getQtyOnHand($data, $field = null)
{
return (int) $this->getString($data, $field);
}
public function getPurchaseCost($data, $field = null)
{
return (float) $this->getString($data, $field);
}
public function getUnitPrice($data, $field = null)
{
return (float) $this->getString($data, $field);
}
}

View File

@ -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;
@ -118,7 +119,7 @@ use Laracasts\Presenter\PresentableTrait;
* @property string|null $smtp_port
* @property string|null $smtp_encryption
* @property string|null $smtp_local_domain
* @property object|null $quickbooks
* @property \App\DataMapper\QuickbooksSettings|null $quickbooks
* @property boolean $smtp_verify_peer
* @property-read \App\Models\Account $account
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Activity> $activities
@ -392,7 +393,7 @@ class Company extends BaseModel
'smtp_username' => 'encrypted',
'smtp_password' => 'encrypted',
'e_invoice' => 'object',
'quickbooks' => 'object',
'quickbooks' => QuickbooksSettings::class,
];
protected $with = [];

View File

@ -30,6 +30,7 @@ use League\CommonMark\CommonMarkConverter;
* @property string|null $custom_value4
* @property string|null $product_key
* @property string|null $notes
* @property string|null $hash
* @property float $cost
* @property float $price
* @property float $quantity

View File

@ -81,7 +81,7 @@ class RouteServiceProvider extends ServiceProvider
if (Ninja::isSelfHost()) {
return Limit::none();
} else {
return Limit::perMinute(25)->by($request->ip());
return Limit::perMinute(10)->by($request->ip());
}
});

View File

@ -154,7 +154,6 @@ class PaymentRepository extends BaseRepository
if ($invoice) {
//25-06-2023
$paymentable = new Paymentable();
$paymentable->payment_id = $payment->id;
$paymentable->paymentable_id = $invoice->id;

View File

@ -109,6 +109,8 @@ class InvoiceService
/**
* Apply a payment amount to an invoice.
*
* *** does not create a paymentable ****
* @param Payment $payment The Payment
* @param float $payment_amount The Payment amount
* @return InvoiceService Parent class object

View File

@ -103,7 +103,6 @@ class MarkPaid extends AbstractService
$this->invoice
->service()
->applyNumber()
// ->deletePdf()
->save();
$payment->ledger()

View File

@ -0,0 +1,308 @@
<?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\Services\Quickbooks\Jobs;
use App\Models\Client;
use App\Models\Company;
use App\Models\Invoice;
use App\Models\Product;
use App\Libraries\MultiDB;
use Illuminate\Bus\Queueable;
use App\Factory\ClientFactory;
use App\Factory\InvoiceFactory;
use App\Factory\ProductFactory;
use App\Factory\ClientContactFactory;
use App\DataMapper\QuickbooksSettings;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use App\Services\Quickbooks\QuickbooksService;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use App\Services\Quickbooks\Transformers\ClientTransformer;
use App\Services\Quickbooks\Transformers\InvoiceTransformer;
use App\Services\Quickbooks\Transformers\PaymentTransformer;
use App\Services\Quickbooks\Transformers\ProductTransformer;
class QuickbooksSync implements ShouldQueue
{
use Dispatchable;
use InteractsWithQueue;
use Queueable;
use SerializesModels;
private array $entities = [
'product' => 'Item',
'client' => 'Customer',
'invoice' => 'Invoice',
'quote' => 'Estimate',
'purchase_order' => 'PurchaseOrder',
'payment' => 'Payment',
];
private QuickbooksService $qbs;
private ?array $settings;
private Company $company;
public function __construct(public int $company_id, public string $db)
{
}
/**
* Execute the job.
*/
public function handle()
{
MultiDB::setDb($this->db);
$this->company = Company::find($this->company_id);
$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;
}
$records = $this->qbs->sdk()->fetchRecords($entity);
$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)
{
match($entity){
// 'client' => $this->syncQbToNinjaClients($records),
'product' => $this->syncQbToNinjaProducts($records),
// 'invoice' => $this->syncQbToNinjaInvoices($records),
// 'vendor' => $this->syncQbToNinjaClients($records),
// 'quote' => $this->syncInvoices($records),
// 'purchase_order' => $this->syncInvoices($records),
// 'payment' => $this->syncPayment($records),
default => false,
};
}
private function syncQbToNinjaInvoices($records): void
{
$invoice_transformer = new InvoiceTransformer($this->company);
foreach($records as $record)
{
$ninja_invoice_data = $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))
{
$invoice->fill($ninja_invoice_data);
$invoice->saveQuietly();
$invoice = $invoice->calc()->getInvoice()->service()->markSent()->save();
foreach($payment_ids as $payment_id)
{
$payment = $this->qbs->sdk->FindById('Payment', $payment_id);
$payment_transformer = new PaymentTransformer($this->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'];
$paymentable->save();
$invoice->service()->applyPayment($ninja_payment, $transformed['applied']);
}
}
$ninja_invoice_data = false;
}
}
private function findInvoice(array $ninja_invoice_data): ?Invoice
{
$search = Invoice::query()
->withTrashed()
->where('company_id', $this->company->id)
->where('number', $ninja_invoice_data['number']);
if($search->count() == 0) {
//new invoice
$invoice = InvoiceFactory::create($this->company->id, $this->company->owner()->id);
$invoice->client_id = $ninja_invoice_data['client_id'];
return $invoice;
} elseif($search->count() == 1) {
return $this->settings['invoice']['update_record'] ? $search->first() : null;
}
return null;
}
private function syncQbToNinjaClients(array $records): void
{
$client_transformer = new ClientTransformer($this->company);
foreach($records as $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 syncQbToNinjaProducts($records): void
{
$product_transformer = new ProductTransformer($this->company);
foreach($records as $record)
{
$ninja_data = $product_transformer->qbToNinja($record);
if($product = $this->findProduct($ninja_data['hash']))
{
$product->fill($ninja_data);
$product->save();
}
}
}
private function findClient(array $qb_data) :?Client
{
$client = $qb_data[0];
$contact = $qb_data[1];
$client_meta = $qb_data[2];
$search = Client::query()
->withTrashed()
->where('company_id', $this->company->id)
->where(function ($q) use ($client, $client_meta, $contact){
$q->where('client_hash', $client_meta['client_hash'])
->orWhere('id_number', $client['id_number'])
->orWhereHas('contacts', function ($q) use ($contact){
$q->where('email', $contact['email']);
});
});
if($search->count() == 0) {
//new client
$client = ClientFactory::create($this->company->id, $this->company->owner()->id);
$client->client_hash = $client_meta['client_hash'];
$client->settings = $client_meta['settings'];
return $client;
}
elseif($search->count() == 1) {
return $this->settings['client']['update_record'] ? $search->first() : null;
}
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()
{
return [new WithoutOverlapping("qbs-{$this->company_id}-{$this->db}")];
}
public function failed($exception)
{
nlog("QuickbooksSync failed => ".$exception->getMessage());
config(['queue.failed.driver' => null]);
}
}

View File

@ -9,11 +9,23 @@
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Services\Import\Quickbooks;
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 QuickBooksOnline\API\Core\CoreConstants;
use QuickBooksOnline\API\DataService\DataService;
use App\Services\Quickbooks\Transformers\ClientTransformer;
use App\Services\Quickbooks\Transformers\InvoiceTransformer;
use App\Services\Quickbooks\Transformers\PaymentTransformer;
use App\Services\Quickbooks\Transformers\ProductTransformer;
// quickbooks_realm_id
// quickbooks_refresh_token
@ -64,14 +76,19 @@ class QuickbooksService
] : [];
}
public function getSdk(): DataService
{
return $this->sdk;
}
public function sdk(): SdkWrapper
{
return new SdkWrapper($this->sdk, $this->company);
}
/**
* //@todo - refactor to a job
*
* @return void
*/
public function syncFromQb()
{
QuickbooksSync::dispatch($this->company);
}
}

View File

@ -9,8 +9,9 @@
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Services\Import\Quickbooks;
namespace App\Services\Quickbooks;
use App\DataMapper\QuickbooksSettings;
use Carbon\Carbon;
use App\Models\Company;
use QuickBooksOnline\API\DataService\DataService;
@ -20,7 +21,7 @@ class SdkWrapper
{
public const MAXRESULTS = 10000;
private $entities = ['Customer','Invoice','Payment','Item'];
private $entities = ['Customer','Invoice','Item'];
private OAuth2AccessToken $token;
@ -55,9 +56,6 @@ class SdkWrapper
public function company()
{
nlog("getting company info");
// nlog($this->sdk->getAccessToken());
return $this->sdk->getCompanyInfo();
}
/*
@ -85,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'),
@ -124,8 +122,6 @@ class SdkWrapper
*/
public function setAccessToken(OAuth2AccessToken $token): self
{
// $this->sdk = $this->sdk->updateOAuth2Token($token);
$this->token = $token;
return $this;
@ -138,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!!
@ -159,12 +155,17 @@ class SdkWrapper
return (int)$this->sdk->Query("select count(*) from $entity");
}
private function queryData(string $query, int $start = 1, $limit = 100): array
private function queryData(string $query, int $start = 1, $limit = 1000): array
{
return (array) $this->sdk->Query($query, $start, $limit);
}
public function fetchRecords(string $entity, int $max = 1000): array
public function fetchById(string $entity, $id)
{
return $this->sdk->FindById($entity, $id);
}
public function fetchRecords(string $entity, int $max = 100000): array
{
if(!in_array($entity, $this->entities)) {
@ -173,7 +174,7 @@ class SdkWrapper
$records = [];
$start = 0;
$limit = 100;
$limit = 1000;
try {
$total = $this->totalRecords($entity);
$total = min($max, $total);
@ -197,6 +198,7 @@ class SdkWrapper
nlog("Fetch Quickbooks API Error: {$th->getMessage()}");
}
nlog($records);
return $records;
}
}

View File

@ -0,0 +1,74 @@
<?php
/**
* Invoice Ninja (https://clientninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Services\Quickbooks\Transformers;
use App\Models\Client;
use App\Models\Company;
/**
* Class BaseTransformer.
*/
class BaseTransformer
{
public function __construct(public Company $company)
{
}
public function resolveCountry(string $iso_3_code): string
{
/** @var \App\Models\Country $country */
$country = app('countries')->first(function ($c) use ($iso_3_code){
/** @var \App\Models\Country $c */
return $c->iso_3166_3 == $iso_3_code;
});
return $country ? (string) $country->id : '840';
}
public function resolveCurrency(string $currency_code): string
{
/** @var \App\Models\Currency $currency */
$currency = app('currencies')->first(function($c) use ($currency_code){
/** @var \App\Models\Currency $c */
return $c->code == $currency_code;
});
return $currency ? (string) $currency->id : '1';
}
public function getShipAddrCountry($data, $field)
{
return is_null(($c = $this->getString($data, $field))) ? null : $this->getCountryId($c);
}
public function getBillAddrCountry($data, $field)
{
return is_null(($c = $this->getString($data, $field))) ? null : $this->getCountryId($c);
}
public function getClientId($customer_reference_id): ?int
{
$client = Client::query()
->withTrashed()
->where('company_id', $this->company->id)
->where('id_number', $customer_reference_id)
->first();
return $client ? $client->id : null;
}
}

View File

@ -0,0 +1,70 @@
<?php
/**
* Invoice Ninja (https://clientninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Services\Quickbooks\Transformers;
use App\DataMapper\ClientSettings;
/**
* Class ClientTransformer.
*/
class ClientTransformer extends BaseTransformer
{
public function qbToNinja(mixed $qb_data)
{
return $this->transform($qb_data);
}
public function ninjaToQb()
{
}
public function transform(mixed $data): array
{
$contact = [
'first_name' => data_get($data, 'GivenName'),
'last_name' => data_get($data, 'FamilyName'),
'phone' => data_get($data, 'PrimaryPhone.FreeFormNumber'),
'email' => data_get($data, 'PrimaryEmailAddr.Address'),
];
$client = [
'name' => data_get($data,'CompanyName', ''),
'address1' => data_get($data, 'BillAddr.Line1', ''),
'address2' => data_get($data, 'BillAddr.Line2', ''),
'city' => data_get($data, 'BillAddr.City', ''),
'country_id' => $this->resolveCountry(data_get($data, 'BillAddr.Country', '')),
'state' => data_get($data, 'BillAddr.CountrySubDivisionCode', ''),
'postal_code' => data_get($data, 'BillAddr.PostalCode', ''),
'shipping_address1' => data_get($data, 'ShipAddr.Line1', ''),
'shipping_address2' => data_get($data, 'ShipAddr.Line2', ''),
'shipping_city' => data_get($data, 'ShipAddr.City', ''),
'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.value', ''),
];
$settings = ClientSettings::defaults();
$settings->currency_id = (string) $this->resolveCurrency(data_get($data, 'CurrencyRef.value'));
$new_client_merge = [
'client_hash' => data_get($data, 'V4IDPseudonym', \Illuminate\Support\Str::random(32)),
'settings' => $settings,
];
return [$client, $contact, $new_client_merge];
}
}

View File

@ -0,0 +1,256 @@
<?php
/**
* Invoice Ninja (https://clientninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Services\Quickbooks\Transformers;
use App\Models\Client;
use App\Models\Company;
use App\Models\Invoice;
use App\Models\Product;
use App\DataMapper\InvoiceItem;
/**
* Class InvoiceTransformer.
*/
class InvoiceTransformer extends BaseTransformer
{
public function qbToNinja(mixed $qb_data)
{
return $this->transform($qb_data);
}
public function ninjaToQb()
{
}
public function transform($qb_data)
{
$client_id = $this->getClientId(data_get($qb_data, 'CustomerRef.value', null));
return $client_id ? [
'client_id' => $client_id,
'number' => data_get($qb_data, 'DocNumber', false),
'date' => data_get($qb_data, 'TxnDate', now()->format('Y-m-d')),
'private_notes' => data_get($qb_data, 'PrivateNote', ''),
'public_notes' => data_get($qb_data, 'CustomerMemo.value', false),
'due_date' => data_get($qb_data, 'DueDate', null),
'po_number' => data_get($qb_data, 'PONumber', ""),
'partial' => data_get($qb_data, 'Deposit', 0),
'line_items' => $this->getLineItems(data_get($qb_data, 'Line', [])),
'payment_ids' => $this->getPayments($qb_data),
'status_id' => Invoice::STATUS_SENT,
'tax_rate1' => $rate = data_get($qb_data,'TxnTaxDetail.TaxLine.TaxLineDetail.TaxPercent', 0),
'tax_name1' => $rate > 0 ? "Sales Tax" : "",
] : false;
}
private function getPayments(mixed $qb_data)
{
$payments = [];
nlog("get payments");
$qb_payments = data_get($qb_data, 'LinkedTxn', false);
nlog($qb_payments);
if(!$qb_payments) {
return [];
}
if(!is_array($qb_payments) && data_get($qb_payments, 'TxnType', false) == 'Payment'){
nlog([data_get($qb_payments, 'TxnId.value', false)]);
return [data_get($qb_payments, 'TxnId.value', false)];
}
foreach($qb_payments as $payment)
{
if(data_get($payment, 'TxnType', false) == 'Payment')
{
$payments[] = data_get($payment, 'TxnId.value', false);
}
}
return $payments;
}
private function getLineItems(mixed $qb_items)
{
$items = [];
foreach($qb_items as $qb_item)
{
$item = new InvoiceItem;
$item->product_key = data_get($qb_item, 'SalesItemLineDetail.ItemRef.name', '');
$item->notes = data_get($qb_item,'Description', '');
$item->quantity = data_get($qb_item,'SalesItemLineDetail.Qty', 0);
$item->cost = data_get($qb_item, 'SalesItemLineDetail.UnitPrice', 0);
$item->discount = data_get($item,'DiscountRate', data_get($qb_item,'DiscountAmount', 0));
$item->is_amount_discount = data_get($qb_item,'DiscountAmount', 0) > 0 ? true : false;
$item->type_id = stripos(data_get($qb_item, 'ItemAccountRef.name'), 'Service') !== false ? '2' : '1';
$item->tax_id = data_get($qb_item, 'TaxCodeRef.value', '') == 'NON' ? Product::PRODUCT_TYPE_EXEMPT : $item->type_id;
$item->tax_rate1 = data_get($qb_item,'TaxLineDetail.TaxRateRef.TaxPercent', 0);
$item->tax_name1 = $item->tax_rate1 > 0 ? "Sales Tax" : "";
$items[] = (object)$item;
}
nlog($items);
return $items;
}
// public function getTotalAmt($data)
// {
// return (float) $this->getString($data, 'TotalAmt');
// }
// public function getLine($data)
// {
// return array_map(function ($item) {
// return [
// 'description' => $this->getString($item, 'Description'),
// 'product_key' => $this->getString($item, 'Description'),
// 'quantity' => (int) $this->getString($item, 'SalesItemLineDetail.Qty'),
// 'unit_price' => (float) $this->getString($item, 'SalesItemLineDetail.UnitPrice'),
// 'line_total' => (float) $this->getString($item, 'Amount'),
// 'cost' => (float) $this->getString($item, 'SalesItemLineDetail.UnitPrice'),
// 'product_cost' => (float) $this->getString($item, 'SalesItemLineDetail.UnitPrice'),
// 'tax_amount' => (float) $this->getString($item, 'TxnTaxDetail.TotalTax'),
// ];
// }, array_filter($this->getString($data, 'Line'), function ($item) {
// return $this->getString($item, 'DetailType') !== 'SubTotalLineDetail';
// }));
// }
// public function getInvoiceClient($data, $field = null)
// {
// /**
// * "CustomerRef": {
// "value": "23",
// "name": ""Barnett Design
// },
// "CustomerMemo": {
// "value": "Thank you for your business and have a great day!"
// },
// "BillAddr": {
// "Id": "58",
// "Line1": "Shara Barnett",
// "Line2": "Barnett Design",
// "Line3": "19 Main St.",
// "Line4": "Middlefield, CA 94303",
// "Lat": "37.4530553",
// "Long": "-122.1178261"
// },
// "ShipAddr": {
// "Id": "24",
// "Line1": "19 Main St.",
// "City": "Middlefield",
// "CountrySubDivisionCode": "CA",
// "PostalCode": "94303",
// "Lat": "37.445013",
// "Long": "-122.1391443"
// },"BillEmail": {
// "Address": "Design@intuit.com"
// },
// [
// 'name' => 'CompanyName',
// 'phone' => 'PrimaryPhone.FreeFormNumber',
// 'country_id' => 'BillAddr.Country',
// 'state' => 'BillAddr.CountrySubDivisionCode',
// 'address1' => 'BillAddr.Line1',
// 'city' => 'BillAddr.City',
// 'postal_code' => 'BillAddr.PostalCode',
// 'shipping_country_id' => 'ShipAddr.Country',
// 'shipping_state' => 'ShipAddr.CountrySubDivisionCode',
// 'shipping_address1' => 'ShipAddr.Line1',
// 'shipping_city' => 'ShipAddr.City',
// 'shipping_postal_code' => 'ShipAddr.PostalCode',
// 'public_notes' => 'Notes'
// ];
// */
// $bill_address = (object) $this->getString($data, 'BillAddr');
// $ship_address = $this->getString($data, 'ShipAddr');
// $customer = explode(" ", $this->getString($data, 'CustomerRef.name'));
// $customer = ['GivenName' => $customer[0], 'FamilyName' => $customer[1]];
// $has_company = property_exists($bill_address, 'Line4');
// $address = $has_company ? $bill_address->Line4 : $bill_address->Line3;
// $address_1 = substr($address, 0, stripos($address, ','));
// $address = array_filter([$address_1] + (explode(' ', substr($address, stripos($address, ",") + 1))));
// $client_id = null;
// $client =
// [
// "CompanyName" => $has_company ? $bill_address->Line2 : $bill_address->Line1,
// "BillAddr" => array_combine(['City','CountrySubDivisionCode','PostalCode'], array_pad($address, 3, 'N/A')) + ['Line1' => $has_company ? $bill_address->Line3 : $bill_address->Line2 ],
// "ShipAddr" => $ship_address
// ] + $customer + ['PrimaryEmailAddr' => ['Address' => $this->getString($data, 'BillEmail.Address') ]];
// if($this->hasClient($client['CompanyName'])) {
// $client_id = $this->getClient($client['CompanyName'], $this->getString($client, 'PrimaryEmailAddr.Address'));
// }
// return ['client' => (new ClientTransformer($this->company))->transform($client), 'client_id' => $client_id ];
// }
// public function getDueDate($data)
// {
// return $this->parseDateOrNull($data, 'DueDate');
// }
// public function getDeposit($data)
// {
// return (float) $this->getString($data, 'Deposit');
// }
// public function getBalance($data)
// {
// return (float) $this->getString($data, 'Balance');
// }
// public function getCustomerMemo($data)
// {
// return $this->getString($data, 'CustomerMemo.value');
// }
// public function getDocNumber($data, $field = null)
// {
// return sprintf(
// "%s-%s",
// $this->getString($data, 'DocNumber'),
// $this->getString($data, 'Id.value')
// );
// }
// public function getLinkedTxn($data)
// {
// $payments = $this->getString($data, 'LinkedTxn');
// if(empty($payments)) {
// return [];
// }
// return [[
// 'amount' => $this->getTotalAmt($data),
// 'date' => $this->parseDateOrNull($data, 'TxnDate')
// ]];
// }
}

View File

@ -0,0 +1,85 @@
<?php
/**
* Invoice Ninja (https://Paymentninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Services\Quickbooks\Transformers;
use App\Models\Company;
use App\Models\Payment;
use App\Factory\PaymentFactory;
/**
*
* Class PaymentTransformer.
*/
class PaymentTransformer extends BaseTransformer
{
public function qbToNinja(mixed $qb_data)
{
return $this->transform($qb_data);
}
public function ninjaToQb()
{
}
public function transform(mixed $qb_data)
{
return [
'date' => data_get($qb_data, 'TxnDate', now()->format('Y-m-d')),
'amount' => floatval(data_get($qb_data, 'TotalAmt', 0)),
'applied' => data_get($qb_data, 'TotalAmt', 0) - data_get($qb_data, 'UnappliedAmt', 0),
'number' => data_get($qb_data, 'DocNumber', null),
'private_notes' => data_get($qb_data, 'PrivateNote', null),
'currency_id' => (string) $this->resolveCurrency(data_get($qb_data, 'CurrencyRef.value')),
'client_id' => $this->getClientId(data_get($qb_data, 'CustomerRef.value', null)),
];
}
public function buildPayment($qb_data): ?Payment
{
$ninja_payment_data = $this->transform($qb_data);
if($ninja_payment_data['client_id'])
{
$payment = PaymentFactory::create($this->company->id, $this->company->owner()->id,$ninja_payment_data['client_id']);
$payment->amount = $ninja_payment_data['amount'];
$payment->applied = $ninja_payment_data['applied'];
$payment->status_id = 4;
$payment->fill($ninja_payment_data);
$payment->client->service()->updatePaidToDate($payment->amount);
return $payment;
}
return null;
}
public function getLine($data, $field = null)
{
$invoices = [];
$invoice = $this->getString($data, 'Line.LinkedTxn.TxnType');
if(is_null($invoice) || $invoice !== 'Invoice') {
return $invoices;
}
if(is_null(($invoice_id = $this->getInvoiceId($this->getString($data, 'Line.LinkedTxn.TxnId.value'))))) {
return $invoices;
}
return [[
'amount' => (float) $this->getString($data, 'Line.Amount'),
'invoice_id' => $invoice_id
]];
}
}

View File

@ -0,0 +1,46 @@
<?php
/**
* Invoice Ninja (https://clientninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Services\Quickbooks\Transformers;
/**
* Class ProductTransformer.
*/
class ProductTransformer extends BaseTransformer
{
public function qbToNinja(mixed $qb_data)
{
return $this->transform($qb_data);
}
public function ninjaToQb()
{
}
public function transform(mixed $data): array
{
nlog(data_get($data, 'Id', null));
return [
'hash' => 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),
'price' => data_get($data, 'UnitPrice', 0),
'in_stock_quantity' => data_get($data, 'QtyOnHand', 0),
];
}
}

View File

@ -122,15 +122,11 @@ return [
'secret' => env('CHORUS_SECRET', false),
],
'quickbooks' => [
// 'auth_mode' => 'oauth2',
'client_id' => env('QUICKBOOKS_CLIENT_ID', false),
'client_secret' => env('QUICKBOOKS_CLIENT_SECRET', false),
// 'ClientID' => env('QUICKBOOKS_CLIENT_ID', false),
// 'ClientSecret' => env('QUICKBOOKS_CLIENT_SECRET', false),
// TODO use env('QUICKBOOKS_REDIRECT_URI') or route()/ url()
// 'RedirectURI' => url("/quickbooks/authorized"),
// 'scope' => "com.intuit.quickbooks.accounting",
// 'baseUrl' => ucfirst(env('APP_URL'))
'debug' => env('APP_DEBUG',false)
],
'quickbooks_webhook' => [
'verifier_token' => env('QUICKBOOKS_VERIFIER_TOKEN', false),
],
];

View File

@ -0,0 +1,26 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('products', function (Blueprint $table){
$table->string('hash')->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
//
}
};

109
public/build/assets/app-234e3402.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -9,7 +9,7 @@
]
},
"resources/js/app.js": {
"file": "assets/app-e0713224.js",
"file": "assets/app-234e3402.js",
"imports": [
"_index-08e160a7.js",
"__commonjsHelpers-725317a4.js"

View File

@ -2,9 +2,9 @@
namespace Tests\Feature\Http\Controllers;
use App\Services\Import\Quickbooks\Contracts\SdkInterface as QuickbooksInterface;
use App\Services\Import\Quickbooks\Service as QuickbooksService;
use App\Services\Import\Quickbooks\SdkWrapper as QuickbooksSDK;
use App\Services\Quickbooks\Contracts\SdkInterface as QuickbooksInterface;
use App\Services\Quickbooks\Service as QuickbooksService;
use App\Services\Quickbooks\SdkWrapper as QuickbooksSDK;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;

View File

@ -30,110 +30,12 @@ class QuickbooksTest extends TestCase
protected function setUp(): void
{
parent::setUp();
$this->markTestSkipped("NO BUENO");
$this->withoutMiddleware(ThrottleRequests::class);
config(['database.default' => config('ninja.db.default')]);
$this->makeTestData();
//
$this->withoutExceptionHandling();
Auth::setUser($this->user);
$this->markTestSkipped('no bueno');
}
public function testImportCallsGetDataOnceForClient()
public function testCustomerSync()
{
$data = (json_decode(file_get_contents(base_path('tests/Feature/Import/customers.json')), true))['Customer'];
$hash = Str::random(32);
Cache::put($hash.'-client', base64_encode(json_encode($data)), 360);
$quickbooks = Mockery::mock(Quickbooks::class, [[
'hash' => $hash,
'column_map' => ['client' => ['mapping' => []]],
'skip_header' => true,
'import_type' => 'quickbooks',
], $this->company ])->makePartial();
$quickbooks->shouldReceive('getData')
->once()
->with('client')
->andReturn($data);
// Mocking the dependencies used within the client method
$quickbooks->import('client');
$this->assertArrayHasKey('clients', $quickbooks->entity_count);
$this->assertGreaterThan(0, $quickbooks->entity_count['clients']);
$base_transformer = new BaseTransformer($this->company);
$this->assertTrue($base_transformer->hasClient('Sonnenschein Family Store'));
$contact = $base_transformer->getClient('Amy\'s Bird Sanctuary', '');
$contact = Client::where('name', 'Amy\'s Bird Sanctuary')->first();
$this->assertEquals('(650) 555-3311', $contact->phone);
$this->assertEquals('Birds@Intuit.com', $contact->contacts()->first()->email);
}
public function testImportCallsGetDataOnceForProducts()
{
$data = (json_decode(file_get_contents(base_path('tests/Feature/Import/items.json')), true))['Item'];
$hash = Str::random(32);
Cache::put($hash.'-item', base64_encode(json_encode($data)), 360);
$quickbooks = Mockery::mock(Quickbooks::class, [[
'hash' => $hash,
'column_map' => ['item' => ['mapping' => []]],
'skip_header' => true,
'import_type' => 'quickbooks',
], $this->company ])->makePartial();
$quickbooks->shouldReceive('getData')
->once()
->with('product')
->andReturn($data);
// Mocking the dependencies used within the client method
$quickbooks->import('product');
$this->assertArrayHasKey('products', $quickbooks->entity_count);
$this->assertGreaterThan(0, $quickbooks->entity_count['products']);
$base_transformer = new BaseTransformer($this->company);
$this->assertTrue($base_transformer->hasProduct('Gardening'));
$product = Product::where('product_key', 'Pest Control')->first();
$this->assertGreaterThanOrEqual(35, $product->price);
$this->assertLessThanOrEqual(0, $product->quantity);
}
public function testImportCallsGetDataOnceForInvoices()
{
$data = (json_decode(file_get_contents(base_path('tests/Feature/Import/invoices.json')), true))['Invoice'];
$hash = Str::random(32);
Cache::put($hash.'-invoice', base64_encode(json_encode($data)), 360);
$quickbooks = Mockery::mock(Quickbooks::class, [[
'hash' => $hash,
'column_map' => ['invoice' => ['mapping' => []]],
'skip_header' => true,
'import_type' => 'quickbooks',
], $this->company ])->makePartial();
$quickbooks->shouldReceive('getData')
->once()
->with('invoice')
->andReturn($data);
$quickbooks->import('invoice');
$this->assertArrayHasKey('invoices', $quickbooks->entity_count);
$this->assertGreaterThan(0, $quickbooks->entity_count['invoices']);
$base_transformer = new BaseTransformer($this->company);
$this->assertTrue($base_transformer->hasInvoice(1007));
$invoice = Invoice::where('number', 1012)->first();
$data = collect($data)->where('DocNumber', '1012')->first();
$this->assertGreaterThanOrEqual($data['TotalAmt'], $invoice->amount);
$this->assertEquals(count($data['Line']) - 1, count((array)$invoice->line_items));
}
protected function tearDown(): void
{
Mockery::close();
parent::tearDown();
$data = (json_decode(file_get_contents(base_path('tests/Feature/Import/Quickbooks/customers.json')), false));
}
}

File diff suppressed because it is too large Load Diff

View File

@ -28,6 +28,7 @@ class QuickbooksIngestTest extends TestCase
parent::setUp();
config(['database.default' => config('ninja.db.default')]);
$this->markTestSkipped('no bueno');
$this->makeTestData();
$this->withoutExceptionHandling();
Auth::setUser($this->user);

View File

@ -2,9 +2,9 @@
namespace Tests\Integration\Services\Import\Quickbooks;
use App\Services\Import\Quickbooks\Contracts\SdkInterface as QuickbooksInterface;
use App\Services\Import\Quickbooks\Service as QuickbooksService;
use App\Services\Import\Quickbooks\SdkWrapper as QuickbooksSDK;
use App\Services\Quickbooks\Contracts\SdkInterface as QuickbooksInterface;
use App\Services\Quickbooks\Service as QuickbooksService;
use App\Services\Quickbooks\SdkWrapper as QuickbooksSDK;
use Illuminate\Support\Collection;
use Illuminate\Support\Arr;
use Tests\TestCase;

View File

@ -7,8 +7,8 @@ namespace Tests\Unit\Services\Import\Quickbooks;
use Mockery;
use Tests\TestCase;
use Illuminate\Support\Arr;
use App\Services\Import\Quickbooks\Contracts\SdkInterface;
use App\Services\Import\Quickbooks\SdkWrapper as QuickbookSDK;
use App\Services\Quickbooks\Contracts\SdkInterface;
use App\Services\Quickbooks\SdkWrapper as QuickbookSDK;
class SdkWrapperTest extends TestCase
{

View File

@ -5,8 +5,8 @@ namespace Tests\Unit\Services\Import\Quickbooks;
use Mockery;
use Tests\TestCase;
use Illuminate\Support\Collection;
use App\Services\Import\Quickbooks\Service as QuickbooksService;
use App\Services\Import\Quickbooks\Contracts\SdkInterface as QuickbooksInterface;
use App\Services\Quickbooks\Service as QuickbooksService;
use App\Services\Quickbooks\Contracts\SdkInterface as QuickbooksInterface;
class ServiceTest extends TestCase
{