mirror of
https://github.com/invoiceninja/invoiceninja.git
synced 2025-07-09 03:14:30 -04:00
commit
df914450e2
50
app/Casts/QuickbooksSettingsCast.php
Normal file
50
app/Casts/QuickbooksSettingsCast.php
Normal 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,
|
||||||
|
])
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
59
app/DataMapper/QuickbooksSettings.php
Normal file
59
app/DataMapper/QuickbooksSettings.php
Normal 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -99,7 +99,7 @@ class TransactionTransformer implements BankRevenueInterface
|
|||||||
} elseif (array_key_exists('internalTransactionId', $transaction)) {
|
} elseif (array_key_exists('internalTransactionId', $transaction)) {
|
||||||
$transactionId = $transaction["internalTransactionId"];
|
$transactionId = $transaction["internalTransactionId"];
|
||||||
} else {
|
} 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');
|
throw new \Exception('invalid dataset: missing transactionId - Please report this error to the developer');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,7 +15,7 @@ use App\Http\Requests\Quickbooks\AuthorizedQuickbooksRequest;
|
|||||||
use App\Libraries\MultiDB;
|
use App\Libraries\MultiDB;
|
||||||
use Illuminate\Support\Facades\Cache;
|
use Illuminate\Support\Facades\Cache;
|
||||||
use App\Http\Requests\Quickbooks\AuthQuickbooksRequest;
|
use App\Http\Requests\Quickbooks\AuthQuickbooksRequest;
|
||||||
use App\Services\Import\Quickbooks\QuickbooksService;
|
use App\Services\Quickbooks\QuickbooksService;
|
||||||
|
|
||||||
class ImportQuickbooksController extends BaseController
|
class ImportQuickbooksController extends BaseController
|
||||||
{
|
{
|
||||||
|
@ -93,6 +93,12 @@ class StoreInvoiceRequest extends Request
|
|||||||
/** @var \App\Models\User $user */
|
/** @var \App\Models\User $user */
|
||||||
$user = auth()->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->all();
|
||||||
|
|
||||||
$input = $this->decodePrimaryKeys($input);
|
$input = $this->decodePrimaryKeys($input);
|
||||||
|
@ -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);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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 ] ;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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')
|
|
||||||
]];
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -13,6 +13,7 @@ namespace App\Models;
|
|||||||
|
|
||||||
use App\Casts\EncryptedCast;
|
use App\Casts\EncryptedCast;
|
||||||
use App\DataMapper\CompanySettings;
|
use App\DataMapper\CompanySettings;
|
||||||
|
use App\DataMapper\QuickbooksSettings;
|
||||||
use App\Models\Presenters\CompanyPresenter;
|
use App\Models\Presenters\CompanyPresenter;
|
||||||
use App\Services\Company\CompanyService;
|
use App\Services\Company\CompanyService;
|
||||||
use App\Services\Notification\NotificationService;
|
use App\Services\Notification\NotificationService;
|
||||||
@ -118,7 +119,7 @@ use Laracasts\Presenter\PresentableTrait;
|
|||||||
* @property string|null $smtp_port
|
* @property string|null $smtp_port
|
||||||
* @property string|null $smtp_encryption
|
* @property string|null $smtp_encryption
|
||||||
* @property string|null $smtp_local_domain
|
* @property string|null $smtp_local_domain
|
||||||
* @property object|null $quickbooks
|
* @property \App\DataMapper\QuickbooksSettings|null $quickbooks
|
||||||
* @property boolean $smtp_verify_peer
|
* @property boolean $smtp_verify_peer
|
||||||
* @property-read \App\Models\Account $account
|
* @property-read \App\Models\Account $account
|
||||||
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Activity> $activities
|
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Activity> $activities
|
||||||
@ -392,7 +393,7 @@ class Company extends BaseModel
|
|||||||
'smtp_username' => 'encrypted',
|
'smtp_username' => 'encrypted',
|
||||||
'smtp_password' => 'encrypted',
|
'smtp_password' => 'encrypted',
|
||||||
'e_invoice' => 'object',
|
'e_invoice' => 'object',
|
||||||
'quickbooks' => 'object',
|
'quickbooks' => QuickbooksSettings::class,
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $with = [];
|
protected $with = [];
|
||||||
|
@ -30,6 +30,7 @@ use League\CommonMark\CommonMarkConverter;
|
|||||||
* @property string|null $custom_value4
|
* @property string|null $custom_value4
|
||||||
* @property string|null $product_key
|
* @property string|null $product_key
|
||||||
* @property string|null $notes
|
* @property string|null $notes
|
||||||
|
* @property string|null $hash
|
||||||
* @property float $cost
|
* @property float $cost
|
||||||
* @property float $price
|
* @property float $price
|
||||||
* @property float $quantity
|
* @property float $quantity
|
||||||
|
@ -81,7 +81,7 @@ class RouteServiceProvider extends ServiceProvider
|
|||||||
if (Ninja::isSelfHost()) {
|
if (Ninja::isSelfHost()) {
|
||||||
return Limit::none();
|
return Limit::none();
|
||||||
} else {
|
} else {
|
||||||
return Limit::perMinute(25)->by($request->ip());
|
return Limit::perMinute(10)->by($request->ip());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -154,7 +154,6 @@ class PaymentRepository extends BaseRepository
|
|||||||
if ($invoice) {
|
if ($invoice) {
|
||||||
|
|
||||||
//25-06-2023
|
//25-06-2023
|
||||||
|
|
||||||
$paymentable = new Paymentable();
|
$paymentable = new Paymentable();
|
||||||
$paymentable->payment_id = $payment->id;
|
$paymentable->payment_id = $payment->id;
|
||||||
$paymentable->paymentable_id = $invoice->id;
|
$paymentable->paymentable_id = $invoice->id;
|
||||||
|
@ -109,6 +109,8 @@ class InvoiceService
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Apply a payment amount to an invoice.
|
* Apply a payment amount to an invoice.
|
||||||
|
*
|
||||||
|
* *** does not create a paymentable ****
|
||||||
* @param Payment $payment The Payment
|
* @param Payment $payment The Payment
|
||||||
* @param float $payment_amount The Payment amount
|
* @param float $payment_amount The Payment amount
|
||||||
* @return InvoiceService Parent class object
|
* @return InvoiceService Parent class object
|
||||||
|
@ -103,7 +103,6 @@ class MarkPaid extends AbstractService
|
|||||||
$this->invoice
|
$this->invoice
|
||||||
->service()
|
->service()
|
||||||
->applyNumber()
|
->applyNumber()
|
||||||
// ->deletePdf()
|
|
||||||
->save();
|
->save();
|
||||||
|
|
||||||
$payment->ledger()
|
$payment->ledger()
|
||||||
|
308
app/Services/Quickbooks/Jobs/QuickbooksSync.php
Normal file
308
app/Services/Quickbooks/Jobs/QuickbooksSync.php
Normal 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]);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -9,11 +9,23 @@
|
|||||||
* @license https://www.elastic.co/licensing/elastic-license
|
* @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\Company;
|
||||||
|
use App\Models\Invoice;
|
||||||
|
use App\Models\Product;
|
||||||
|
use App\Services\Quickbooks\Jobs\QuickbooksSync;
|
||||||
use QuickBooksOnline\API\Core\CoreConstants;
|
use QuickBooksOnline\API\Core\CoreConstants;
|
||||||
use QuickBooksOnline\API\DataService\DataService;
|
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_realm_id
|
||||||
// quickbooks_refresh_token
|
// quickbooks_refresh_token
|
||||||
@ -64,14 +76,19 @@ class QuickbooksService
|
|||||||
] : [];
|
] : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getSdk(): DataService
|
|
||||||
{
|
|
||||||
return $this->sdk;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function sdk(): SdkWrapper
|
public function sdk(): SdkWrapper
|
||||||
{
|
{
|
||||||
return new SdkWrapper($this->sdk, $this->company);
|
return new SdkWrapper($this->sdk, $this->company);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* //@todo - refactor to a job
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function syncFromQb()
|
||||||
|
{
|
||||||
|
QuickbooksSync::dispatch($this->company);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -9,8 +9,9 @@
|
|||||||
* @license https://www.elastic.co/licensing/elastic-license
|
* @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 Carbon\Carbon;
|
||||||
use App\Models\Company;
|
use App\Models\Company;
|
||||||
use QuickBooksOnline\API\DataService\DataService;
|
use QuickBooksOnline\API\DataService\DataService;
|
||||||
@ -20,7 +21,7 @@ class SdkWrapper
|
|||||||
{
|
{
|
||||||
public const MAXRESULTS = 10000;
|
public const MAXRESULTS = 10000;
|
||||||
|
|
||||||
private $entities = ['Customer','Invoice','Payment','Item'];
|
private $entities = ['Customer','Invoice','Item'];
|
||||||
|
|
||||||
private OAuth2AccessToken $token;
|
private OAuth2AccessToken $token;
|
||||||
|
|
||||||
@ -55,9 +56,6 @@ class SdkWrapper
|
|||||||
|
|
||||||
public function company()
|
public function company()
|
||||||
{
|
{
|
||||||
nlog("getting company info");
|
|
||||||
// nlog($this->sdk->getAccessToken());
|
|
||||||
|
|
||||||
return $this->sdk->getCompanyInfo();
|
return $this->sdk->getCompanyInfo();
|
||||||
}
|
}
|
||||||
/*
|
/*
|
||||||
@ -85,10 +83,10 @@ class SdkWrapper
|
|||||||
/**
|
/**
|
||||||
* Set Stored NinjaAccessToken
|
* Set Stored NinjaAccessToken
|
||||||
*
|
*
|
||||||
* @param mixed $token_object
|
* @param QuickbooksSettings $token_object
|
||||||
* @return self
|
* @return self
|
||||||
*/
|
*/
|
||||||
public function setNinjaAccessToken(mixed $token_object): self
|
public function setNinjaAccessToken(QuickbooksSettings $token_object): self
|
||||||
{
|
{
|
||||||
$token = new OAuth2AccessToken(
|
$token = new OAuth2AccessToken(
|
||||||
config('services.quickbooks.client_id'),
|
config('services.quickbooks.client_id'),
|
||||||
@ -124,8 +122,6 @@ class SdkWrapper
|
|||||||
*/
|
*/
|
||||||
public function setAccessToken(OAuth2AccessToken $token): self
|
public function setAccessToken(OAuth2AccessToken $token): self
|
||||||
{
|
{
|
||||||
// $this->sdk = $this->sdk->updateOAuth2Token($token);
|
|
||||||
|
|
||||||
$this->token = $token;
|
$this->token = $token;
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
@ -138,7 +134,7 @@ class SdkWrapper
|
|||||||
|
|
||||||
public function saveOAuthToken(OAuth2AccessToken $token): void
|
public function saveOAuthToken(OAuth2AccessToken $token): void
|
||||||
{
|
{
|
||||||
$obj = new \stdClass();
|
$obj = $this->company->quickbooks ?? new QuickbooksSettings();
|
||||||
$obj->accessTokenKey = $token->getAccessToken();
|
$obj->accessTokenKey = $token->getAccessToken();
|
||||||
$obj->refresh_token = $token->getRefreshToken();
|
$obj->refresh_token = $token->getRefreshToken();
|
||||||
$obj->accessTokenExpiresAt = Carbon::createFromFormat('Y/m/d H:i:s', $token->getAccessTokenExpiresAt())->timestamp; //@phpstan-ignore-line - QB phpdoc wrong types!!
|
$obj->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");
|
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);
|
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)) {
|
if(!in_array($entity, $this->entities)) {
|
||||||
@ -173,7 +174,7 @@ class SdkWrapper
|
|||||||
|
|
||||||
$records = [];
|
$records = [];
|
||||||
$start = 0;
|
$start = 0;
|
||||||
$limit = 100;
|
$limit = 1000;
|
||||||
try {
|
try {
|
||||||
$total = $this->totalRecords($entity);
|
$total = $this->totalRecords($entity);
|
||||||
$total = min($max, $total);
|
$total = min($max, $total);
|
||||||
@ -197,6 +198,7 @@ class SdkWrapper
|
|||||||
nlog("Fetch Quickbooks API Error: {$th->getMessage()}");
|
nlog("Fetch Quickbooks API Error: {$th->getMessage()}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
nlog($records);
|
||||||
return $records;
|
return $records;
|
||||||
}
|
}
|
||||||
}
|
}
|
74
app/Services/Quickbooks/Transformers/BaseTransformer.php
Normal file
74
app/Services/Quickbooks/Transformers/BaseTransformer.php
Normal 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
70
app/Services/Quickbooks/Transformers/ClientTransformer.php
Normal file
70
app/Services/Quickbooks/Transformers/ClientTransformer.php
Normal 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];
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
256
app/Services/Quickbooks/Transformers/InvoiceTransformer.php
Normal file
256
app/Services/Quickbooks/Transformers/InvoiceTransformer.php
Normal 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')
|
||||||
|
// ]];
|
||||||
|
|
||||||
|
// }
|
||||||
|
}
|
85
app/Services/Quickbooks/Transformers/PaymentTransformer.php
Normal file
85
app/Services/Quickbooks/Transformers/PaymentTransformer.php
Normal 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
|
||||||
|
]];
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
46
app/Services/Quickbooks/Transformers/ProductTransformer.php
Normal file
46
app/Services/Quickbooks/Transformers/ProductTransformer.php
Normal 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),
|
||||||
|
];
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -122,15 +122,11 @@ return [
|
|||||||
'secret' => env('CHORUS_SECRET', false),
|
'secret' => env('CHORUS_SECRET', false),
|
||||||
],
|
],
|
||||||
'quickbooks' => [
|
'quickbooks' => [
|
||||||
// 'auth_mode' => 'oauth2',
|
|
||||||
'client_id' => env('QUICKBOOKS_CLIENT_ID', false),
|
'client_id' => env('QUICKBOOKS_CLIENT_ID', false),
|
||||||
'client_secret' => env('QUICKBOOKS_CLIENT_SECRET', 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)
|
'debug' => env('APP_DEBUG',false)
|
||||||
],
|
],
|
||||||
];
|
'quickbooks_webhook' => [
|
||||||
|
'verifier_token' => env('QUICKBOOKS_VERIFIER_TOKEN', false),
|
||||||
|
],
|
||||||
|
];
|
@ -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
109
public/build/assets/app-234e3402.js
vendored
Normal file
File diff suppressed because one or more lines are too long
109
public/build/assets/app-e0713224.js
vendored
109
public/build/assets/app-e0713224.js
vendored
File diff suppressed because one or more lines are too long
@ -9,7 +9,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"resources/js/app.js": {
|
"resources/js/app.js": {
|
||||||
"file": "assets/app-e0713224.js",
|
"file": "assets/app-234e3402.js",
|
||||||
"imports": [
|
"imports": [
|
||||||
"_index-08e160a7.js",
|
"_index-08e160a7.js",
|
||||||
"__commonjsHelpers-725317a4.js"
|
"__commonjsHelpers-725317a4.js"
|
||||||
|
@ -2,9 +2,9 @@
|
|||||||
|
|
||||||
namespace Tests\Feature\Http\Controllers;
|
namespace Tests\Feature\Http\Controllers;
|
||||||
|
|
||||||
use App\Services\Import\Quickbooks\Contracts\SdkInterface as QuickbooksInterface;
|
use App\Services\Quickbooks\Contracts\SdkInterface as QuickbooksInterface;
|
||||||
use App\Services\Import\Quickbooks\Service as QuickbooksService;
|
use App\Services\Quickbooks\Service as QuickbooksService;
|
||||||
use App\Services\Import\Quickbooks\SdkWrapper as QuickbooksSDK;
|
use App\Services\Quickbooks\SdkWrapper as QuickbooksSDK;
|
||||||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
use Illuminate\Foundation\Testing\WithFaker;
|
use Illuminate\Foundation\Testing\WithFaker;
|
||||||
|
@ -30,110 +30,12 @@ class QuickbooksTest extends TestCase
|
|||||||
protected function setUp(): void
|
protected function setUp(): void
|
||||||
{
|
{
|
||||||
parent::setUp();
|
parent::setUp();
|
||||||
|
$this->markTestSkipped('no bueno');
|
||||||
$this->markTestSkipped("NO BUENO");
|
|
||||||
$this->withoutMiddleware(ThrottleRequests::class);
|
|
||||||
config(['database.default' => config('ninja.db.default')]);
|
|
||||||
$this->makeTestData();
|
|
||||||
//
|
|
||||||
$this->withoutExceptionHandling();
|
|
||||||
Auth::setUser($this->user);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testImportCallsGetDataOnceForClient()
|
public function testCustomerSync()
|
||||||
{
|
{
|
||||||
$data = (json_decode(file_get_contents(base_path('tests/Feature/Import/customers.json')), true))['Customer'];
|
$data = (json_decode(file_get_contents(base_path('tests/Feature/Import/Quickbooks/customers.json')), false));
|
||||||
$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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
11675
tests/Feature/Import/Quickbooks/customer.json
Normal file
11675
tests/Feature/Import/Quickbooks/customer.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -28,6 +28,7 @@ class QuickbooksIngestTest extends TestCase
|
|||||||
parent::setUp();
|
parent::setUp();
|
||||||
|
|
||||||
config(['database.default' => config('ninja.db.default')]);
|
config(['database.default' => config('ninja.db.default')]);
|
||||||
|
$this->markTestSkipped('no bueno');
|
||||||
$this->makeTestData();
|
$this->makeTestData();
|
||||||
$this->withoutExceptionHandling();
|
$this->withoutExceptionHandling();
|
||||||
Auth::setUser($this->user);
|
Auth::setUser($this->user);
|
||||||
|
@ -2,9 +2,9 @@
|
|||||||
|
|
||||||
namespace Tests\Integration\Services\Import\Quickbooks;
|
namespace Tests\Integration\Services\Import\Quickbooks;
|
||||||
|
|
||||||
use App\Services\Import\Quickbooks\Contracts\SdkInterface as QuickbooksInterface;
|
use App\Services\Quickbooks\Contracts\SdkInterface as QuickbooksInterface;
|
||||||
use App\Services\Import\Quickbooks\Service as QuickbooksService;
|
use App\Services\Quickbooks\Service as QuickbooksService;
|
||||||
use App\Services\Import\Quickbooks\SdkWrapper as QuickbooksSDK;
|
use App\Services\Quickbooks\SdkWrapper as QuickbooksSDK;
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
use Illuminate\Support\Arr;
|
use Illuminate\Support\Arr;
|
||||||
use Tests\TestCase;
|
use Tests\TestCase;
|
||||||
|
@ -7,8 +7,8 @@ namespace Tests\Unit\Services\Import\Quickbooks;
|
|||||||
use Mockery;
|
use Mockery;
|
||||||
use Tests\TestCase;
|
use Tests\TestCase;
|
||||||
use Illuminate\Support\Arr;
|
use Illuminate\Support\Arr;
|
||||||
use App\Services\Import\Quickbooks\Contracts\SdkInterface;
|
use App\Services\Quickbooks\Contracts\SdkInterface;
|
||||||
use App\Services\Import\Quickbooks\SdkWrapper as QuickbookSDK;
|
use App\Services\Quickbooks\SdkWrapper as QuickbookSDK;
|
||||||
|
|
||||||
class SdkWrapperTest extends TestCase
|
class SdkWrapperTest extends TestCase
|
||||||
{
|
{
|
||||||
|
@ -5,8 +5,8 @@ namespace Tests\Unit\Services\Import\Quickbooks;
|
|||||||
use Mockery;
|
use Mockery;
|
||||||
use Tests\TestCase;
|
use Tests\TestCase;
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
use App\Services\Import\Quickbooks\Service as QuickbooksService;
|
use App\Services\Quickbooks\Service as QuickbooksService;
|
||||||
use App\Services\Import\Quickbooks\Contracts\SdkInterface as QuickbooksInterface;
|
use App\Services\Quickbooks\Contracts\SdkInterface as QuickbooksInterface;
|
||||||
|
|
||||||
class ServiceTest extends TestCase
|
class ServiceTest extends TestCase
|
||||||
{
|
{
|
||||||
|
Loading…
x
Reference in New Issue
Block a user