Merge pull request #9917 from M-E-Development-Design/feature/import-quickbooks

Feature/import quickbooks
This commit is contained in:
David Bomba 2024-08-21 12:00:02 +10:00 committed by GitHub
commit 37822d7a0d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
47 changed files with 14312 additions and 4 deletions

View File

@ -0,0 +1,63 @@
<?php
namespace App\Factory;
use App\Libraries\MultiDB;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Cache;
use QuickBooksOnline\API\DataService\DataService;
use App\Services\Import\Quickbooks\Repositories\CompanyTokensRepository;
class QuickbooksSDKFactory
{
public static function create()
{
$tokens = [];
// Ensure the user is authenticated
if(($user = Auth::user()))
{
$company = $user->company();
$token_store = (new CompanyTokensRepository($company->company_key));
$tokens = array_filter($token_store->get());
if(!empty($tokens)) {
$keys = ['refreshTokenKey','QBORealmID'];
if(array_key_exists('access_token', $tokens)) {
$keys = array_merge(['accessTokenKey'] ,$keys);
}
$tokens = array_combine($keys, array_values($tokens));
}
}
$config = $tokens + config('services.quickbooks.settings') + [
'state' => Str::random(12)
];
$sdk = DataService::Configure($config);
if (env('APP_DEBUG')) {
$sdk->setLogLocation(storage_path("logs/quickbooks.log"));
$sdk->enableLog();
}
$sdk->setMinorVersion("73");
$sdk->throwExceptionOnError(true);
if(array_key_exists('refreshTokenKey', $config) && !array_key_exists('accessTokenKey', $config))
{
$tokens = ($sdk->getOAuth2LoginHelper())->refreshToken();
$sdk = $sdk->updateOAuth2Token($tokens);
$tokens = ($sdk->getOAuth2LoginHelper())->getAccessToken();
$access_token = $tokens->getAccessToken();
$realm = $tokens->getRealmID();
$refresh_token = $tokens->getRefreshToken();
$access_token_expires = $tokens->getAccessTokenExpiresAt();
$refresh_token_expires = $tokens->getRefreshTokenExpiresAt();
$tokens = compact('access_token', 'refresh_token','access_token_expires', 'refresh_token_expires','realm');
$token_store->save($tokens);
}
return $sdk;
}
}

View File

@ -0,0 +1,199 @@
<?php
namespace App\Http\Controllers;
use \Closure;
use App\Utils\Ninja;
use App\Models\Company;
use Illuminate\Support\Str;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use App\Utils\Traits\MakesHash;
use Illuminate\Support\Facades\Cache;
use App\Jobs\Import\QuickbooksIngest;
use Illuminate\Support\Facades\Validator;
use App\Services\Import\Quickbooks\Service as QuickbooksService;
class ImportQuickbooksController extends BaseController
{
protected QuickbooksService $service;
private $import_entities = [
'client' => 'Customer',
'invoice' => 'Invoice',
'product' => 'Item',
'payment' => 'Payment'
];
public function __construct(QuickbooksService $service) {
parent::__construct();
$this->service = $service;
$this->middleware(
function (Request $request, Closure $next) {
// Check for the required query parameters
if (!$request->has(['code', 'state', 'realmId'])) {
return abort(400,'Unauthorized');
}
$rules = [
'state' => [
'required',
'valid' => function ($attribute, $value, $fail) {
if (!Cache::has($value)) {
$fail('The state is invalid.');
}
},
]
];
// Custom error messages
$messages = [
'state.required' => 'The state is required.',
'state.valid' => 'state token not valid'
];
// Perform the validation
$validator = Validator::make($request->all(), $rules, $messages);
if ($validator->fails()) {
// If validation fails, redirect back with errors and input
return redirect('/')
->withErrors($validator)
->withInput();
}
$token = Cache::pull($request->state);
$request->merge(['company' => Cache::get($token) ]);
return $next($request);
}
)->only('onAuthorized');
$this->middleware(
function ( Request $request, Closure $next) {
$rules = [
'token' => [
'required',
'valid' => function ($attribute, $value, $fail) {
if (!Cache::has($value) || (!Company::where('company_key', (Cache::get($value))['company_key'])->exists() )) {
$fail('The company is invalid.');
}
},
]
];
// Custom error messages
$messages = [
'token.required' => 'The token is required.',
'token.valid' => 'Token note valid!'
];
// Perform the validation
$validator = Validator::make(['token' => $request->token ], $rules, $messages);
if ($validator->fails()) {
return redirect()
->back()
->withErrors($validator)
->withInput();
}
//If validation passes, proceed to the next middleware/controller
return $next($request);
}
)->only('authorizeQuickbooks');
}
public function onAuthorized(Request $request)
{
$realm = $request->query('realmId');
$company_key = $request->input('company.company_key');
$company_id = $request->input('company.id');
$tokens = ($auth_service = $this->service->getOAuth())->accessToken($request->query('code'), $realm);
$auth_service->saveTokens($company_key, ['realm' => $realm] + $tokens);
return response(200);
}
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorizeQuickbooks(Request $request)
{
$token = $request->token;
$auth = $this->service->getOAuth();
$authorizationUrl = $auth->getAuthorizationUrl();
$state = $auth->getState();
Cache::put($state, $token, 190);
return redirect()->to($authorizationUrl);
}
public function preimport(string $type, string $hash)
{
// Check for authorization otherwise
// Create a reference
$data = [
'hash' => $hash,
'type' => $type
];
$this->getData($data);
}
protected function getData($data) {
$entity = $this->import_entities[$data['type']];
$cache_name = "{$data['hash']}-{$data['type']}";
// TODO: Get or put cache or DB?
if(! Cache::has($cache_name) )
{
$contents = call_user_func([$this->service, "fetch{$entity}s"]);
if($contents->isEmpty()) return;
Cache::put($cache_name, base64_encode( $contents->toJson()), 600);
}
}
/**
* @OA\Post(
* path="/api/v1/import_json",
* operationId="getImportJson",
* tags={"import"},
* summary="Import data from the system",
* description="Import data from the system",
* @OA\Parameter(ref="#/components/parameters/X-Requested-With"),
* @OA\Response(
* response=200,
* description="success",
* @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"),
* @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"),
* @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"),
* ),
* @OA\Response(
* response=422,
* description="Validation error",
* @OA\JsonContent(ref="#/components/schemas/ValidationError"),
* ),
* @OA\Response(
* response="default",
* description="Unexpected Error",
* @OA\JsonContent(ref="#/components/schemas/Error"),
* ),
* )
*/
public function import(Request $request)
{
$hash = Str::random(32);
foreach($request->input('import_types') as $type)
{
$this->preimport($type, $hash);
}
/** @var \App\Models\User $user */
$user = auth()->user() ?? Auth::loginUsingId(60);
$data = ['import_types' => $request->input('import_types') ] + compact('hash');
if (Ninja::isHosted()) {
QuickbooksIngest::dispatch( $data , $user->company() );
} else {
QuickbooksIngest::dispatch($data, $user->company() );
}
return response()->json(['message' => 'Processing'], 200);
}
}

View File

@ -0,0 +1,252 @@
<?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\Import\Providers;
use App\Models\Invoice;
use App\Factory\ProductFactory;
use App\Factory\ClientFactory;
use App\Factory\InvoiceFactory;
use App\Factory\PaymentFactory;
use Illuminate\Support\Facades\Cache;
use App\Repositories\ClientRepository;
use App\Repositories\InvoiceRepository;
use App\Repositories\ProductRepository;
use App\Repositories\PaymentRepository;
use App\Http\Requests\Client\StoreClientRequest;
use App\Http\Requests\Product\StoreProductRequest;
use App\Http\Requests\Invoice\StoreInvoiceRequest;
use App\Http\Requests\Payment\StorePaymentRequest;
use App\Import\Transformer\Quickbooks\ClientTransformer;
use App\Import\Transformer\Quickbooks\InvoiceTransformer;
use App\Import\Transformer\Quickbooks\ProductTransformer;
use App\Import\Transformer\Quickbooks\PaymentTransformer;
class Quickbooks extends BaseImport
{
public array $entity_count = [];
public function import(string $entity)
{
if (
in_array($entity, [
'client',
'invoice',
'product',
'payment',
// 'vendor',
// 'expense',
])
) {
$this->{$entity}();
}
//collate any errors
// $this->finalizeImport();
}
public function client()
{
$entity_type = 'client';
$data = $this->getData($entity_type);
if (empty($data)) {
$this->entity_count['clients'] = 0;
return;
}
$this->request_name = StoreClientRequest::class;
$this->repository_name = ClientRepository::class;
$this->factory_name = ClientFactory::class;
$this->repository = app()->make($this->repository_name);
$this->repository->import_mode = true;
$this->transformer = new ClientTransformer($this->company);
$client_count = $this->ingest($data, $entity_type);
$this->entity_count['clients'] = $client_count;
}
public function product()
{
$entity_type = 'product';
$data = $this->getData($entity_type);
if (empty($data)) {
$this->entity_count['products'] = 0;
return;
}
$this->request_name = StoreProductRequest::class;
$this->repository_name = ProductRepository::class;
$this->factory_name = ProductFactory::class;
$this->repository = app()->make($this->repository_name);
$this->repository->import_mode = true;
$this->transformer = new ProductTransformer($this->company);
$count = $this->ingest($data, $entity_type);
$this->entity_count['products'] = $count;
}
public function getData($type) {
// get the data from cache? file? or api ?
return json_decode(base64_decode(Cache::get("{$this->hash}-$type")), 1);
}
public function payment()
{
$entity_type = 'payment';
$data = $this->getData($entity_type);
if (empty($data)) {
$this->entity_count['payments'] = 0;
return;
}
$this->request_name = StorePaymentRequest::class;
$this->repository_name = PaymentRepository::class;
$this->factory_name = PaymentFactory::class;
$this->repository = app()->make($this->repository_name);
$this->repository->import_mode = true;
$this->transformer = new PaymentTransformer($this->company);
$count = $this->ingest($data, $entity_type);
$this->entity_count['payments'] = $count;
}
public function invoice()
{
//make sure we update and create products
$initial_update_products_value = $this->company->update_products;
$this->company->update_products = true;
$this->company->save();
$entity_type = 'invoice';
$data = $this->getData($entity_type);
if (empty($data)) {
$this->entity_count['invoices'] = 0;
return;
}
$this->request_name = StoreInvoiceRequest::class;
$this->repository_name = InvoiceRepository::class;
$this->factory_name = InvoiceFactory::class;
$this->repository = app()->make($this->repository_name);
$this->repository->import_mode = true;
$this->transformer = new InvoiceTransformer($this->company);
$invoice_count = $this->ingestInvoices($data,'');
$this->entity_count['invoices'] = $invoice_count;
$this->company->update_products = $initial_update_products_value;
$this->company->save();
}
public function ingestInvoices($invoices, $invoice_number_key)
{
$count = 0;
$invoice_transformer = $this->transformer;
/** @var ClientRepository $client_repository */
$client_repository = app()->make(ClientRepository::class);
$client_repository->import_mode = true;
$invoice_repository = new InvoiceRepository();
$invoice_repository->import_mode = true;
foreach ($invoices as $raw_invoice) {
if(!is_array($raw_invoice)) {
continue;
}
try {
$invoice_data = $invoice_transformer->transform($raw_invoice);
$invoice_data['user_id'] = $this->company->owner()->id;
$invoice_data['line_items'] = (array) $invoice_data['line_items'];
$invoice_data['line_items'] = $this->cleanItems(
$invoice_data['line_items'] ?? []
);
if (
empty($invoice_data['client_id']) &&
! empty($invoice_data['client'])
) {
$client_data = $invoice_data['client'];
$client_data['user_id'] = $this->getUserIDForRecord(
$invoice_data
);
$client_repository->save(
$client_data,
$client = ClientFactory::create(
$this->company->id,
$client_data['user_id']
)
);
$invoice_data['client_id'] = $client->id;
unset($invoice_data['client']);
}
$validator = $this->request_name::runFormRequest($invoice_data);
if ($validator->fails()) {
$this->error_array['invoice'][] = [
'invoice' => $invoice_data,
'error' => $validator->errors()->all(),
];
} else {
if(!Invoice::where('number',$invoice_data['number'])->get()->first())
{
$invoice = InvoiceFactory::create(
$this->company->id,
$this->company->owner()->id
);
$invoice->mergeFillable(['partial','amount','balance','line_items']);
if (! empty($invoice_data['status_id'])) {
$invoice->status_id = $invoice_data['status_id'];
}
$saveable_invoice_data = $invoice_data;
if(array_key_exists('payments', $saveable_invoice_data)) {
unset($saveable_invoice_data['payments']);
}
$invoice->fill($saveable_invoice_data);
$invoice->save();
$count++;
}
// $this->actionInvoiceStatus(
// $invoice,
// $invoice_data,
// $invoice_repository
// );
}
} catch (\Exception $ex) {
if (\DB::connection(config('database.default'))->transactionLevel() > 0) {
\DB::connection(config('database.default'))->rollBack();
}
if ($ex instanceof ImportException) {
$message = $ex->getMessage();
} else {
report($ex);
$message = 'Unknown error ';
nlog($ex->getMessage());
nlog($raw_invoice);
}
$this->error_array['invoice'][] = [
'invoice' => $raw_invoice,
'error' => $message,
];
}
}
return $count;
}
}

View File

@ -0,0 +1,95 @@
<?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 ];
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
]);
}
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

@ -0,0 +1,36 @@
<?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

@ -0,0 +1,196 @@
<?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 Illuminate\Support\Str;
use Illuminate\Support\Arr;
use App\Import\ImportException;
use App\DataMapper\InvoiceItem;
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' =>(double) $this->getString($item,'SalesItemLineDetail.UnitPrice'),
'line_total' => (double) $this->getString($item,'Amount'),
'cost' =>(double) $this->getString($item,'SalesItemLineDetail.UnitPrice'),
'product_cost' => (double) $this->getString($item,'SalesItemLineDetail.UnitPrice'),
'tax_amount' => (double) $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 (double) $this->getString($data,'Deposit');
}
public function getBalance($data)
{
return (double) $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,99 @@
<?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

@ -0,0 +1,59 @@
<?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 (double) $this->getString($data, $field);
}
public function getUnitPrice($data, $field = null) {
return (float) $this->getString($data, $field);
}
}

View File

@ -0,0 +1,45 @@
<?php
namespace App\Jobs\Import;
use App\Libraries\MultiDB;
use Illuminate\Bus\Queueable;
use App\Import\Providers\Quickbooks;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
class QuickbooksIngest implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $engine;
protected $request;
protected $company;
/**
* Create a new job instance.
*/
public function __construct(array $request, $company)
{
$this->company = $company;
$this->request = $request;
}
/**
* Execute the job.
*/
public function handle(): void
{
MultiDB::setDb($this->company->db);
set_time_limit(0);
$engine = new Quickbooks(['import_type' => 'client', 'hash'=> $this->request['hash'] ], $this->company);
foreach ($this->request['import_types'] as $entity) {
$engine->import($entity);
}
$engine->finalizeImport();
}
}

View File

@ -0,0 +1,84 @@
<?php
namespace App\Providers;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Route;
use App\Factory\QuickbooksSDKFactory;
use Illuminate\Support\ServiceProvider;
use App\Http\Controllers\ImportQuickbooksController;
use App\Services\Import\Quickbooks\Service as QuickbooksService;
use App\Repositories\Import\Quickcbooks\Contracts\RepositoryInterface;
use App\Services\Import\Quickbooks\SdkWrapper as QuickbooksSDKWrapper;
use App\Services\Import\Quickbooks\Contracts\SdkInterface as QuickbooksInterface;
use App\Services\Import\Quickbooks\Transformers\Transformer as QuickbooksTransformer;
class QuickbooksServiceProvider extends ServiceProvider
{
/**
* Register services.
*
* @return void
*/
public function register()
{
$this->app->bind(QuickbooksInterface::class, function ($app) {
return new QuickbooksSDKWrapper(QuickbooksSDKFactory::create());
});
// Register SDKWrapper with DataService dependency
$this->app->singleton(QuickbooksService::class, function ($app) {
return new QuickbooksService($app->make(QuickbooksInterface::class));
});
$this->app->singleton(QuickbooksTransformer::class,QuickbooksTransformer::class);
}
/**
* Bootstrap services.
*
* @return void
*/
public function boot()
{
$this->registerRoutes();
$this->registerConfig();
}
protected function registerConfig() {
config()->set( 'services.quickbooks' ,
['settings' => [
'auth_mode' => 'oauth2',
'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_ENV'))
],
'debug' => env('APP_DEBUG') || env('APP_ENV')
]
);
}
/**
* Register custom routes.
*
* @return void
*/
protected function registerRoutes()
{
Route::middleware('web')
->namespace($this->app->getNamespace() . 'Http\Controllers')
->group(function () {
Route::get('quickbooks/authorize/{token}', [ImportQuickbooksController::class, 'authorizeQuickbooks'])->name('authorize.quickbooks');
Route::get('quickbooks/authorized', [ImportQuickbooksController::class, 'onAuthorized'])->name('authorized.quickbooks');
});
Route::prefix('api/v1')
->middleware('api')
->namespace($this->app->getNamespace() . 'Http\Controllers')
->group(function () {
Route::post('import/quickbooks', [ImportQuickbooksController::class, 'import'])->name('import.quickbooks');
});
}
}

View File

@ -0,0 +1,12 @@
<?php
namespace App\Repositories\Import\Quickbooks\Contracts;
use Illuminate\Support\Collection;
interface RepositoryInterface {
function get(int $max = 100): Collection;
function all(): Collection;
function count(): int;
}

View File

@ -0,0 +1,11 @@
<?php
namespace App\Repositories\Import\Quickbooks;
use App\Repositories\Import\Quickbooks\Repository;
use App\Repositories\Import\Quickbooks\Contracts\RepositoryInterface;
class CustomerRepository extends Repository implements RepositoryInterface
{
protected string $entity = "Customer";
}

View File

@ -0,0 +1,10 @@
<?php
namespace App\Repositories\Import\Quickbooks;
use App\Repositories\Import\Quickbooks\Contracts\RepositoryInterface as QuickbooksInterface;
class InvoiceRepository extends Repository implements QuickbooksInterface
{
protected string $entity = "Invoice";
}

View File

@ -0,0 +1,11 @@
<?php
namespace App\Repositories\Import\Quickbooks;
use App\Repositories\Import\Quickbooks\Repository;
use App\Repositories\Import\Quickbooks\Contracts\RepositoryInterface;
class ItemRepository extends Repository implements RepositoryInterface
{
protected string $entity = "Item";
}

View File

@ -0,0 +1,10 @@
<?php
namespace App\Repositories\Import\Quickbooks;
use App\Repositories\Import\Quickbooks\Contracts\RepositoryInterface as QuickbooksInterface;
class PaymentRepository extends Repository implements QuickbooksInterface
{
protected string $entity = "Payment";
}

View File

@ -0,0 +1,38 @@
<?php
namespace App\Repositories\Import\Quickbooks;
use Illuminate\Support\Collection;
use App\Repositories\Import\Quickbooks\Contracts\RepositoryInterface;
use App\Services\Import\Quickbooks\Contracts\SdkInterface as QuickbooksInterface;
use App\Repositories\Import\Quickbooks\Transformers\Transformer as QuickbooksTransformer;
abstract class Repository implements RepositoryInterface
{
protected string $entity;
protected QuickbooksInterface $db;
protected QuickbooksTransformer $transfomer;
public function __construct(QuickbooksInterface $db, QuickbooksTransformer $transfomer)
{
$this->db= $db;
$this->transformer = $transfomer;
}
public function count() : int {
return $this->db->totalRecords($this->entity);
}
public function all() : Collection
{
return $this->get($this->count());
}
public function get(int $max = 100): Collection
{
return $this->transformer->transform($this->db->fetchRecords($this->entity, $max), $this->entity);
}
}

View File

@ -0,0 +1,86 @@
<?php
namespace App\Repositories\Import\Quickbooks\Transformers;
use Illuminate\Support\Collection;
class Transformer
{
public function transform(array $items, string $type): Collection
{
if(!method_exists($this, ($method = "transform{$type}s"))) throw new \InvalidArgumentException("Unknown type: $type");
return call_user_func([$this, $method], $items);
}
protected function transformCustomers(array $items): Collection
{
return $this->transformation($items, [
'CompanyName',
'PrimaryPhone',
'BillAddr',
'ShipAddr',
'Notes',
'GivenName',
'FamilyName',
'PrimaryEmailAddr',
'CurrencyRef',
'MetaData'
]);
}
protected function transformInvoices(array $items): Collection
{
return $this->transformation($items, [
"TotalAmt",
"Line",
"DueDate",
"Deposit",
"Balance",
"CustomerMemo",
"DocNumber",
"CustomerRef",
"BillEmail",
'MetaData',
"BillAddr",
"ShipAddr",
"LinkedTxn",
"Id",
"CurrencyRef",
"TxnTaxDetail",
"TxnDate"
]);
}
protected function transformPayments(array $items): Collection
{
return $this->transformation($items, [
"PaymentRefNum",
"TotalAmt",
"CustomerRef",
"CurrencyRef",
"TxnDate",
"Line",
"PrivateNote",
"MetaData"
]);
}
protected function transformItems(array $items): Collection
{
return $this->transformation($items, [
'Name',
'Description',
'PurchaseCost',
'UnitPrice',
'QtyOnHand',
'MetaData'
]);
}
protected function transformation(array $items, array $keys) : Collection
{
return collect($items)->select($keys);
}
}

View File

@ -0,0 +1,66 @@
<?php
namespace App\Services\Import\Quickbooks;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache;
use App\Services\Import\Quickbooks\Repositories\CompanyTokensRepository;
use App\Services\Import\QuickBooks\Contracts\SDKInterface as QuickbooksInterface;
final class Auth
{
private QuickbooksInterface $sdk;
public function __construct(QuickbooksInterface $quickbooks) {
$this->sdk = $quickbooks;
}
public function accessToken(string $code, string $realm ) : array
{
// TODO: Get or put token in Cache or DB?
return $this->sdk->accessToken($code, $realm);
}
public function refreshToken() : array
{
// TODO: Get or put token in Cache or DB?
return $this->sdk->refreshToken();
}
public function getAuthorizationUrl(): string
{
return $this->sdk->getAuthorizationUrl();
}
public function getState() : string
{
return $this->sdk->getState();
}
public function saveTokens($key, $tokens)
{
$token_store = new CompanyTokensRepository($key);
$token_store->save($tokens);
}
public function getAccessToken() : array
{
$token_store = new CompanyTokensRepository();
$tokens = $token_store->get();
if(empty($tokens)) {
$token = $this->sdk->getAccessToken();
$access_token = $token->getAccessToken();
$realm = $token->getRealmID();
$refresh_token = $token->getRefreshToken();
$access_token_expires = $token->getAccessTokenExpiresAt();
$refresh_token_expires = $token->getRefreshTokenExpiresAt();
$tokens = compact('access_token', 'refresh_token','access_token_expires', 'refresh_token_expires','realm');
}
return $tokens;
}
public function getRefreshToken() : array
{
return $this->getAccessToken();
}
}

View File

@ -0,0 +1,14 @@
<?php
namespace App\Services\Import\Quickbooks\Contracts;
interface SdkInterface
{
function getAuthorizationUrl(): string;
function accessToken(string $code, string $realm): array;
function refreshToken(): array;
function getAccessToken(): array;
function getRefreshToken(): array;
function totalRecords(string $entity): int;
function fetchRecords(string $entity, int $max): array;
}

View File

@ -0,0 +1,76 @@
<?php
namespace App\Services\Import\Quickbooks\Repositories;
use App\Models\Company;
use App\Libraries\MultiDB;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Cache;
class CompanyTokensRepository {
private $company_key;
private $store_key = "quickbooks-token";
public function __construct(string $key = null) {
$this->company_key = $key ?? auth()->user->company()->company_key ?? null;
$this->store_key .= $key;
$this->setCompanyDbByKey();
}
public function save(array $tokens) {
$this->updateAccessToken($tokens['access_token'], $tokens['access_token_expires']);
$this->updateRefreshToken($tokens['refresh_token'], $tokens['refresh_token_expires'], $tokens['realm']);
}
public function findByCompanyKey(): ?Company
{
return Company::where('company_key', $this->company_key)->first();
}
public function setCompanyDbByKey()
{
MultiDB::findAndSetDbByCompanyKey($this->company_key);
}
public function get() {
return $this->getAccessToken() + $this->getRefreshToken();
}
protected function updateRefreshToken(string $token, string $expires, string $realm)
{
DB::table('companies')
->where('company_key', $this->company_key)
->update(['quickbooks_refresh_token' => $token,
'quickbooks_realm_id' => $realm,
'quickbooks_refresh_expires' => $expires ]);
}
protected function updateAccessToken(string $token, string $expires )
{
Cache::put([$this->store_key => $token], $expires);
}
protected function getAccessToken( )
{
$result = Cache::get($this->store_key);
return $result ? ['access_token' => $result] : [];
}
protected function getRefreshToken()
{
$result = (array) DB::table('companies')
->select('quickbooks_refresh_token', 'quickbooks_realm_id')
->where('company_key',$this->company_key)
->where('quickbooks_refresh_expires','>',now())
->first();
return $result? array_combine(['refresh_token','realm'], array_values($result) ) : [];
}
}

View File

@ -0,0 +1,107 @@
<?php
namespace App\Services\Import\Quickbooks;
use App\Services\Import\Quickbooks\Contracts\SdkInterface as QuickbooksInterface;
final class SdkWrapper implements QuickbooksInterface
{
const MAXRESULTS = 10000;
private $sdk;
private $entities = ['Customer','Invoice','Payment','Item'];
public function __construct($sdk)
{
// Prep Data Services
$this->sdk = $sdk;
}
public function getAuthorizationUrl() : string
{
return ($this->sdk->getOAuth2LoginHelper())->getAuthorizationCodeURL();
}
public function getState() : string
{
return ($this->sdk->getOAuth2LoginHelper())->getState();
}
public function getAccessToken() : array
{
return $this->getTokens();
}
public function getRefreshToken(): array{
return $this->getTokens();
}
public function accessToken(string $code, string $realm) : array
{
$token = ($this->sdk->getOAuth2LoginHelper())->exchangeAuthorizationCodeForToken($code,$realm);
return $this->getTokens();
}
private function getTokens() : array {
$token =($this->sdk->getOAuth2LoginHelper())->getAccessToken();
$access_token = $token->getAccessToken();
$refresh_token = $token->getRefreshToken();
$access_token_expires = $token->getAccessTokenExpiresAt();
$refresh_token_expires = $token->getRefreshTokenExpiresAt();
return compact('access_token', 'refresh_token','access_token_expires', 'refresh_token_expires');
}
public function refreshToken(): array
{
$token = ($this->sdk->getOAuth2LoginHelper())->refreshToken();
$this->sdk = $this->sdk->updateOAuth2Token($token);
return $this->getTokens();
}
public function handleCallbacks(array $data): void {
}
public function totalRecords(string $entity) : int {
return $this->sdk->Query("select count(*) from $entity");
}
private function queryData(string $query, int $start = 1, $limit = 100) : array
{
return (array) $this->sdk->Query($query, $start, $limit);
}
public function fetchRecords( string $entity, int $max = 1000): array {
if(!in_array($entity, $this->entities)) return [];
$records = [];
$start = 0;
$limit = 100;
try {
$total = $this->totalRecords($entity);
$total = min($max, $total);
// Step 3 & 4: Get chunks of records until the total required records are retrieved
do {
$limit = min(self::MAXRESULTS, $total - $start);
$recordsChunk = $this->queryData("select * from $entity", $start, $limit);
if(empty($recordsChunk)) break;
$records = array_merge($records,$recordsChunk);
$start += $limit;
} while ($start < $total);
if(empty($records)) throw new \Exceptions("No records retrieved!");
} catch (\Throwable $th) {
nlog("Fetch Quickbooks API Error: {$th->getMessage()}");
}
return $records;
}
}

View File

@ -0,0 +1,86 @@
<?php
namespace App\Services\Import\Quickbooks;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache;
use App\Services\Import\Quickbooks\Auth;
use App\Repositories\Import\Quickbooks\Contracts\RepositoryInterface;
use App\Services\Import\QuickBooks\Contracts\SdkInterface as QuickbooksInterface;
final class Service
{
private QuickbooksInterface $sdk;
public function __construct(QuickbooksInterface $quickbooks) {
$this->sdk = $quickbooks;
}
public function getOAuth() : Auth
{
return new Auth($this->sdk);
}
public function getAccessToken() : array
{
return $this->getOAuth()->getAccessToken();
}
public function getRefreshToken() : array
{
// TODO: Check if token is Cached otherwise fetch a new one and Cache token and expire
return $this->getAccessToken();
}
/**
* fetch QuickBooks invoice records
* @param int $max The maximum records to fetch. Default 100
* @return Illuminate\Support\Collection;
*/
public function fetchInvoices(int $max = 100): Collection
{
return $this->fetchRecords('Invoice', $max) ;
}
/**
* fetch QuickBooks payment records
* @param int $max The maximum records to fetch. Default 100
* @return Illuminate\Support\Collection;
*/
public function fetchPayments(int $max = 100): Collection
{
return $this->fetchRecords('Payment', $max) ;
}
/**
* fetch QuickBooks product records
* @param int $max The maximum records to fetch. Default 100
* @return Illuminate\Support\Collection;
*/
public function fetchItems(int $max = 100): Collection
{
return $this->fetchRecords('Item', $max) ;
}
protected function fetchRecords(string $entity, $max = 100) : Collection {
return (self::RepositoryFactory($entity))->get($max);
}
private static function RepositoryFactory(string $entity) : RepositoryInterface
{
return app("\\App\\Repositories\\Import\Quickbooks\\{$entity}Repository");
}
/**
* fetch QuickBooks customer records
* @param int $max The maximum records to fetch. Default 100
* @return Illuminate\Support\Collection;
*/
public function fetchCustomers(int $max = 100): Collection
{
return $this->fetchRecords('Customer', $max) ;
}
public function totalRecords(string $entity) : int
{
return (self::RepositoryFactory($entity))->count();
}
}

View File

@ -63,6 +63,7 @@
"imdhemy/laravel-purchases": "^1.7", "imdhemy/laravel-purchases": "^1.7",
"intervention/image": "^2.5", "intervention/image": "^2.5",
"invoiceninja/einvoice": "dev-main", "invoiceninja/einvoice": "dev-main",
"invoiceninja/inspector": "^3.0",
"invoiceninja/ubl_invoice": "^2", "invoiceninja/ubl_invoice": "^2",
"josemmo/facturae-php": "^1.7", "josemmo/facturae-php": "^1.7",
"laracasts/presenter": "^0.2.1", "laracasts/presenter": "^0.2.1",
@ -85,6 +86,7 @@
"predis/predis": "^2", "predis/predis": "^2",
"psr/http-message": "^1.0", "psr/http-message": "^1.0",
"pusher/pusher-php-server": "^7.2", "pusher/pusher-php-server": "^7.2",
"quickbooks/v3-php-sdk": "6.1.4-alpha",
"razorpay/razorpay": "2.*", "razorpay/razorpay": "2.*",
"sentry/sentry-laravel": "^4", "sentry/sentry-laravel": "^4",
"setasign/fpdf": "^1.8", "setasign/fpdf": "^1.8",
@ -197,6 +199,10 @@
{ {
"type": "vcs", "type": "vcs",
"url": "https://github.com/turbo124/snappdf" "url": "https://github.com/turbo124/snappdf"
},
{
"type":"vcs",
"url":"https://github.com/karneaud/QuickBooks-V3-PHP-SDK.git"
} }
], ],
"minimum-stability": "dev", "minimum-stability": "dev",

130
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "95e7bd229644d1d8e768ecfbc78582cd", "content-hash": "9e7ea46cfef2848f4eac13cc9c0c679a",
"packages": [ "packages": [
{ {
"name": "adrienrn/php-mimetyper", "name": "adrienrn/php-mimetyper",
@ -4048,6 +4048,68 @@
}, },
"time": "2024-07-22T02:40:27+00:00" "time": "2024-07-22T02:40:27+00:00"
}, },
{
"name": "invoiceninja/inspector",
"version": "v3.0",
"source": {
"type": "git",
"url": "https://github.com/invoiceninja/inspector.git",
"reference": "29bc1ee7ae9d967287ecbd3485a2fee41a13e65f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/invoiceninja/inspector/zipball/29bc1ee7ae9d967287ecbd3485a2fee41a13e65f",
"reference": "29bc1ee7ae9d967287ecbd3485a2fee41a13e65f",
"shasum": ""
},
"require": {
"doctrine/dbal": "^4.0",
"illuminate/support": "^11.0",
"php": "^8.2"
},
"require-dev": {
"orchestra/testbench": "^9.1",
"phpunit/phpunit": "^11.1"
},
"type": "library",
"extra": {
"laravel": {
"providers": [
"InvoiceNinja\\Inspector\\InspectorServiceProvider"
],
"aliases": {
"Inspector": "InvoiceNinja\\Inspector\\InspectorFacade"
}
}
},
"autoload": {
"psr-4": {
"InvoiceNinja\\Inspector\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Benjamin Beganović",
"email": "benjamin.beganovic4@outlook.com",
"role": "Developer"
}
],
"description": "Simplified database records management",
"homepage": "https://github.com/invoiceninja/inspector",
"keywords": [
"inspector",
"invoiceninja"
],
"support": {
"issues": "https://github.com/invoiceninja/inspector/issues",
"source": "https://github.com/invoiceninja/inspector/tree/v3.0"
},
"time": "2024-06-04T12:31:47+00:00"
},
{ {
"name": "invoiceninja/ubl_invoice", "name": "invoiceninja/ubl_invoice",
"version": "v2.2.2", "version": "v2.2.2",
@ -9089,6 +9151,72 @@
}, },
"time": "2023-12-15T10:58:53+00:00" "time": "2023-12-15T10:58:53+00:00"
}, },
{
"name": "quickbooks/v3-php-sdk",
"version": "v6.1.4-alpha",
"source": {
"type": "git",
"url": "https://github.com/karneaud/QuickBooks-V3-PHP-SDK.git",
"reference": "89ff2b6dcfc94634cf5806cacda1286a6898249f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/karneaud/QuickBooks-V3-PHP-SDK/zipball/89ff2b6dcfc94634cf5806cacda1286a6898249f",
"reference": "89ff2b6dcfc94634cf5806cacda1286a6898249f",
"shasum": ""
},
"require": {
"ext-dom": "*",
"ext-mbstring": "*",
"php": ">=5.6.0"
},
"require-dev": {
"php-mock/php-mock": "^2.3",
"php-mock/php-mock-phpunit": "^2.6",
"phpunit/phpunit": "^5.0 || ^6.0 || ^7.0 || ^8"
},
"suggest": {
"ext-curl": "Uses Curl to make HTTP Requests",
"guzzlehttp/guzzle": "Uses Guzzle to make HTTP Requests"
},
"type": "library",
"autoload": {
"psr-4": {
"QuickBooksOnline\\API\\": "src/"
}
},
"archive": {
"exclude": [
"/docs",
"/src/Utility.Test",
"/src/XSD2PHP/docs",
"/src/XSD2PHP/test",
"/test"
]
},
"license": [
"Apache-2.0"
],
"authors": [
{
"name": "abisalehalliprasan",
"email": "anil_kumar3@intuit.com"
}
],
"description": "The Official PHP SDK for QuickBooks Online Accounting API",
"homepage": "http://developer.intuit.com",
"keywords": [
"api",
"http",
"quickbooks",
"rest",
"smallbusiness"
],
"support": {
"source": "https://github.com/karneaud/QuickBooks-V3-PHP-SDK/tree/v6.1.4-alpha"
},
"time": "2024-08-16T01:21:19+00:00"
},
{ {
"name": "ralouphie/getallheaders", "name": "ralouphie/getallheaders",
"version": "3.0.3", "version": "3.0.3",

View File

@ -200,7 +200,8 @@ return [
App\Providers\MultiDBProvider::class, App\Providers\MultiDBProvider::class,
App\Providers\ClientPortalServiceProvider::class, App\Providers\ClientPortalServiceProvider::class,
App\Providers\NinjaTranslationServiceProvider::class, App\Providers\NinjaTranslationServiceProvider::class,
App\Providers\StaticServiceProvider::class App\Providers\StaticServiceProvider::class,
App\Providers\QuickbooksServiceProvider::class
], ],
/* /*
@ -217,7 +218,7 @@ return [
'aliases' => Facade::defaultAliases()->merge([ 'aliases' => Facade::defaultAliases()->merge([
'Collector' => Turbo124\Beacon\CollectorFacade::class, 'Collector' => Turbo124\Beacon\CollectorFacade::class,
'CustomMessage' => App\Utils\ClientPortal\CustomMessage\CustomMessageFacade::class, 'CustomMessage' => App\Utils\ClientPortal\CustomMessage\CustomMessageFacade::class,
'Redis' => Illuminate\Support\Facades\Redis::class, 'Redis' => Illuminate\Support\Facades\Redis::class
])->toArray(), ])->toArray(),
]; ];

View File

@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('companies', function (Blueprint $table) {
$table->string('quickbooks_realm_id')->nullable();
$table->string('quickbooks_refresh_token')->nullable();
$table->dateTime('quickbooks_refresh_expires')->nullable();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('companies', function (Blueprint $table) {
$table->dropColumn(['quickbooks_realm_id', 'quickbooks_refresh_token','quickbooks_refresh_expires']);
});
}
};

View File

@ -57,6 +57,7 @@ use App\Http\Controllers\SystemLogController;
use App\Http\Controllers\TwoFactorController; use App\Http\Controllers\TwoFactorController;
use App\Http\Controllers\Auth\LoginController; use App\Http\Controllers\Auth\LoginController;
use App\Http\Controllers\ImportJsonController; use App\Http\Controllers\ImportJsonController;
use App\Http\Controllers\ImportQuickbooksController;
use App\Http\Controllers\SelfUpdateController; use App\Http\Controllers\SelfUpdateController;
use App\Http\Controllers\TaskStatusController; use App\Http\Controllers\TaskStatusController;
use App\Http\Controllers\Bank\YodleeController; use App\Http\Controllers\Bank\YodleeController;
@ -244,7 +245,7 @@ Route::group(['middleware' => ['throttle:api', 'api_db', 'token_auth', 'locale']
Route::post('import', [ImportController::class, 'import'])->name('import.import'); Route::post('import', [ImportController::class, 'import'])->name('import.import');
Route::post('import_json', [ImportJsonController::class, 'import'])->name('import.import_json'); Route::post('import_json', [ImportJsonController::class, 'import'])->name('import.import_json');
Route::post('preimport', [ImportController::class, 'preimport'])->name('import.preimport'); Route::post('preimport', [ImportController::class, 'preimport'])->name('import.preimport');
;
Route::resource('invoices', InvoiceController::class); // name = (invoices. index / create / show / update / destroy / edit Route::resource('invoices', InvoiceController::class); // name = (invoices. index / create / show / update / destroy / edit
Route::get('invoices/{invoice}/delivery_note', [InvoiceController::class, 'deliveryNote'])->name('invoices.delivery_note'); Route::get('invoices/{invoice}/delivery_note', [InvoiceController::class, 'deliveryNote'])->name('invoices.delivery_note');
Route::get('invoices/{invoice}/{action}', [InvoiceController::class, 'action'])->name('invoices.action'); Route::get('invoices/{invoice}/{action}', [InvoiceController::class, 'action'])->name('invoices.action');

View File

@ -6,6 +6,7 @@ use App\Http\Controllers\Auth\ResetPasswordController;
use App\Http\Controllers\Bank\NordigenController; use App\Http\Controllers\Bank\NordigenController;
use App\Http\Controllers\Bank\YodleeController; use App\Http\Controllers\Bank\YodleeController;
use App\Http\Controllers\BaseController; use App\Http\Controllers\BaseController;
use App\Http\Controllers\ImportQuickbooksController;
use App\Http\Controllers\ClientPortal\ApplePayDomainController; use App\Http\Controllers\ClientPortal\ApplePayDomainController;
use App\Http\Controllers\Gateways\Checkout3dsController; use App\Http\Controllers\Gateways\Checkout3dsController;
use App\Http\Controllers\Gateways\GoCardlessController; use App\Http\Controllers\Gateways\GoCardlessController;

View File

@ -0,0 +1,126 @@
<?php
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 Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Support\Facades\Session;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Bus;
use GuzzleHttp\Psr7\Message;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
use Mockery\MockInterface;
use Tests\MockAccountData;
use Tests\TestCase;
use Mockery;
class ImportQuickbooksControllerTest extends TestCase
{
use MockAccountData;
use DatabaseTransactions;
private $mock;
private $state;
protected function setUp(): void {
parent::setUp();
$this->state = Str::random(4);
$this->mock = Mockery::mock(stdClass::class);
$this->makeTestData();
Session::start();
//app()->singleton(QuickbooksInterface::class, fn() => new QuickbooksSDK($this->mock));
}
public function testAuthorize(): void
{
$this->mock->shouldReceive('getState')->andReturn($this->state);
$this->mock->shouldReceive('getAuthorizationCodeURL')->andReturn('https://example.com');
$this->mock->shouldReceive("getOAuth2LoginHelper")->andReturn($this->mock);
Cache::spy();
Cache::shouldReceive('get')
->with($token = $this->company->company_key)
->andReturn( ['company_key' => $token, 'id' => $this->company->id]);
Cache::shouldReceive('has')
->andReturn(true);
// Perform the test
$response = $this->get(route('authorize.quickbooks', ['token' => $token]));
$response->assertStatus(302);
Cache::shouldHaveReceived('put')->once()->with($this->state, $token, 90);
}
public function testOnAuthorized(): void
{
$token = ['company_key' => $this->company->company_key, 'id' => $this->company->id] ;
$this->mock->shouldReceive('getAccessToken')->andReturn(Mockery::mock(stdClass::class,function(MockInterface $mock){
$mock->shouldReceive('getAccessToken')->andReturn('abcdefg');
$mock->shouldReceive('getRefreshToken')->andReturn('abcdefghi');
$mock->shouldReceive('getAccessTokenExpiresAt')->andReturn(3600);
$mock->shouldReceive('getRefreshTokenExpiresAt')->andReturn(8726400);
}));
$this->mock->shouldReceive("getOAuth2LoginHelper")->andReturn($this->mock);
$this->mock->shouldReceive('exchangeAuthorizationCodeForToken')->once();
Cache::spy();
Cache::shouldReceive('has')
->andReturn(true);
Cache::shouldReceive('get')->andReturn($token);
Cache::shouldReceive('pull')->andReturn($token['company_key']);
// Perform the test
$response = $this->get("/quickbooks/authorized/?code=123456&state={$this->state}&realmId=12345678");
$response->assertStatus(200);
Cache::shouldHaveReceived('put')->once()->with($token['company_key'], 'abcdefg', 3600);
$this->mock->shouldHaveReceived('exchangeAuthorizationCodeForToken')->once()->with(123456,12345678);
}
public function testImport(): void
{
// Cache::spy();
//Bus::fake();
$data = $this->setUpTestData('customers');
$count = count($data);
$this->mock->shouldReceive('Query')->andReturnUsing(
function($val, $s = 1, $max = 1000) use ($count, $data) {
if(stristr($val, 'count')) {
return $count;
}
return Arr::take($data,$max);
}
);
// Perform the test
$response = $this->actingAs($this->user)->withHeaders([
'X-API-TOKEN' => $this->token,
])->post('/api/v1/import/quickbooks',[
'import_types' => ['client']
]);
$response->assertStatus(200);
//Cache::shouldHaveReceived('has')->once()->with("{$hash}-client");
//Bus::assertDispatched(\App\Jobs\Import\QuickbooksIngest::class);
}
protected function setUpTestData($file) {
$data = json_decode(
file_get_contents(base_path("tests/Mock/Quickbooks/Data/$file.json")),true
);
return $data;
}
}

View File

@ -0,0 +1,139 @@
<?php
namespace Tests\Feature\Import\Quickbooks;
use Tests\TestCase;
use App\Import\Providers\Quickbooks;
use App\Import\Transformer\BaseTransformer;
use App\Utils\Traits\MakesHash;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Routing\Middleware\ThrottleRequests;
use Tests\MockAccountData;
use Illuminate\Support\Facades\Cache;
use Mockery;
use App\Models\Client;
use App\Models\Product;
use App\Models\Invoice;
use Illuminate\Support\Str;
use ReflectionClass;
use Illuminate\Support\Facades\Auth;
class QuickbooksTest extends TestCase
{
use MakesHash;
use MockAccountData;
use DatabaseTransactions;
protected $quickbooks;
protected $data;
protected function setUp(): void
{
parent::setUp();
$this->withoutMiddleware(ThrottleRequests::class);
config(['database.default' => config('ninja.db.default')]);
$this->makeTestData();
//
$this->withoutExceptionHandling();
Auth::setUser($this->user);
}
public function testImportCallsGetDataOnceForClient()
{
$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();
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,204 @@
{
"Item": [
{
"Name": "Concrete",
"Description": "Concrete for fountain installation",
"UnitPrice": 0,
"sparse": true,
"Id": "3",
"MetaData": {
"CreateTime": "2024-06-16T10:36:03-07:00",
"LastUpdatedTime": "2024-06-19T12:47:47-07:00"
}
},
{
"Name": "Design",
"Description": "Custom Design",
"UnitPrice": 75,
"sparse": true,
"Id": "4",
"MetaData": {
"CreateTime": "2024-06-16T10:41:38-07:00",
"LastUpdatedTime": "2024-06-16T10:41:38-07:00"
}
},
{
"Name": "Gardening",
"Description": "Weekly Gardening Service",
"UnitPrice": 0,
"sparse": true,
"Id": "6",
"MetaData": {
"CreateTime": "2024-06-16T10:43:14-07:00",
"LastUpdatedTime": "2024-06-16T10:43:14-07:00"
}
},
{
"Name": "Hours",
"UnitPrice": 0,
"sparse": true,
"Id": "2",
"MetaData": {
"CreateTime": "2024-06-11T14:42:05-07:00",
"LastUpdatedTime": "2024-06-11T14:42:05-07:00"
}
},
{
"Name": "Installation",
"Description": "Installation of landscape design",
"UnitPrice": 50,
"sparse": true,
"Id": "7",
"MetaData": {
"CreateTime": "2024-06-16T10:43:54-07:00",
"LastUpdatedTime": "2024-06-16T10:43:54-07:00"
}
},
{
"Name": "Lighting",
"Description": "Garden Lighting",
"UnitPrice": 0,
"sparse": true,
"Id": "8",
"MetaData": {
"CreateTime": "2024-06-16T10:44:40-07:00",
"LastUpdatedTime": "2024-06-19T12:47:38-07:00"
}
},
{
"Name": "Maintenance & Repair",
"Description": "Maintenance & Repair",
"UnitPrice": 0,
"sparse": true,
"Id": "9",
"MetaData": {
"CreateTime": "2024-06-16T10:45:18-07:00",
"LastUpdatedTime": "2024-06-16T10:45:18-07:00"
}
},
{
"Name": "Pest Control",
"Description": "Pest Control Services",
"UnitPrice": 35,
"sparse": true,
"Id": "10",
"MetaData": {
"CreateTime": "2024-06-16T10:45:49-07:00",
"LastUpdatedTime": "2024-06-16T10:45:49-07:00"
}
},
{
"Name": "Pump",
"Description": "Fountain Pump",
"UnitPrice": 15,
"QtyOnHand": 25,
"sparse": true,
"Id": "11",
"MetaData": {
"CreateTime": "2024-06-16T10:46:45-07:00",
"LastUpdatedTime": "2024-06-19T13:16:17-07:00"
}
},
{
"Name": "Refunds & Allowances",
"Description": "Income due to refunds or allowances",
"UnitPrice": 0,
"sparse": true,
"Id": "12",
"MetaData": {
"CreateTime": "2024-06-16T10:49:18-07:00",
"LastUpdatedTime": "2024-06-16T10:49:18-07:00"
}
},
{
"Name": "Rock Fountain",
"Description": "Rock Fountain",
"UnitPrice": 275,
"QtyOnHand": 2,
"sparse": true,
"Id": "5",
"MetaData": {
"CreateTime": "2024-06-16T10:42:19-07:00",
"LastUpdatedTime": "2024-06-19T13:16:17-07:00"
}
},
{
"Name": "Rocks",
"Description": "Garden Rocks",
"UnitPrice": 0,
"sparse": true,
"Id": "13",
"MetaData": {
"CreateTime": "2024-06-16T10:50:11-07:00",
"LastUpdatedTime": "2024-06-19T12:47:31-07:00"
}
},
{
"Name": "Services",
"UnitPrice": 0,
"sparse": true,
"Id": "1",
"MetaData": {
"CreateTime": "2024-06-11T14:42:05-07:00",
"LastUpdatedTime": "2024-06-11T14:42:05-07:00"
}
},
{
"Name": "Sod",
"Description": "Sod",
"UnitPrice": 0,
"sparse": true,
"Id": "14",
"MetaData": {
"CreateTime": "2024-06-16T10:50:45-07:00",
"LastUpdatedTime": "2024-06-19T12:47:22-07:00"
}
},
{
"Name": "Soil",
"Description": "2 cubic ft. bag",
"UnitPrice": 10,
"sparse": true,
"Id": "15",
"MetaData": {
"CreateTime": "2024-06-16T10:51:28-07:00",
"LastUpdatedTime": "2024-06-19T12:47:25-07:00"
}
},
{
"Name": "Sprinkler Heads",
"Description": "Sprinkler Heads",
"UnitPrice": 2,
"QtyOnHand": 25,
"sparse": true,
"Id": "16",
"MetaData": {
"CreateTime": "2024-06-16T10:51:50-07:00",
"LastUpdatedTime": "2024-06-19T12:51:47-07:00"
}
},
{
"Name": "Sprinkler Pipes",
"Description": "Sprinkler Pipes",
"UnitPrice": 4,
"QtyOnHand": 31,
"sparse": true,
"Id": "17",
"MetaData": {
"CreateTime": "2024-06-16T10:52:07-07:00",
"LastUpdatedTime": "2024-06-19T12:57:24-07:00"
}
},
{
"Name": "Trimming",
"Description": "Tree and Shrub Trimming",
"UnitPrice": 35,
"sparse": true,
"Id": "18",
"MetaData": {
"CreateTime": "2024-06-16T10:52:42-07:00",
"LastUpdatedTime": "2024-06-16T10:52:42-07:00"
}
}
]
}

View File

@ -0,0 +1,53 @@
<?php
namespace Tests\Feature\Jobs\Import;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Support\Facades\Cache;
use App\Jobs\Import\QuickbooksIngest;
use Illuminate\Support\Facades\Auth;
use App\Utils\Traits\MakesHash;
use Illuminate\Support\Str;
use Tests\MockAccountData;
use App\Models\Client;
use ReflectionClass;
use Tests\TestCase;
class QuickbooksIngestTest extends TestCase
{
use MakesHash;
use MockAccountData;
use DatabaseTransactions;
protected $quickbooks;
protected function setUp(): void
{
parent::setUp();
config(['database.default' => config('ninja.db.default')]);
$this->makeTestData();
$this->withoutExceptionHandling();
Auth::setUser($this->user);
}
/**
* A basic feature test example.
*/
public function testCanQuickbooksIngest(): void
{
$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);
QuickbooksIngest::dispatch([
'hash' => $hash,
'column_map' => ['client' => ['mapping' => []]],
'skip_header' => true,
'import_types' => ['client'],
], $this->company )->handle();
$this->assertTrue(Client::withTrashed()->where(['company_id' => $this->company->id, 'name' => "Freeman Sporting Goods"])->exists());
}
}

View File

@ -0,0 +1,48 @@
<?php
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 Illuminate\Support\Collection;
use Illuminate\Support\Arr;
use Tests\TestCase;
use Mockery;
class QuickBooksServiceTest extends TestCase
{
private $service;
protected function setUp(): void
{
parent::setUp();
$data = json_decode(
file_get_contents(base_path('tests/Mock/Quickbooks/Data/customers.json')),true
);
$count = count($data);
$sdkMock = Mockery::mock(sdtClass::class);
$sdkMock->shouldReceive('Query')->andReturnUsing(function($val) use ($count, $data) {
if(stristr($val, 'count')) {
return $count;
}
return Arr::take($data,4);
});
app()->singleton(QuickbooksInterface::class, fn() => new QuickbooksSDK($sdkMock));
$this->service = app(QuickbooksService::class);
}
public function testImportCustomers()
{
$collection = $this->service->fetchCustomers(4);
$this->assertInstanceOf(Collection::class, $collection);
$this->assertEquals(4, $collection->count());
$this->assertNotNull($item = $collection->whereStrict('CompanyName', "Cool Cars")->first());
$this->assertEquals("Grace", $item['GivenName']);
}
}

View File

@ -0,0 +1,48 @@
{
"Customer":{
"Taxable": false,
"BillAddr": {
"Id": "4",
"Line1": "65 Ocean Dr.",
"City": "Half Moon Bay",
"CountrySubDivisionCode": "CA",
"PostalCode": "94213",
"Lat": "37.4300318",
"Long": "-122.4336537"
},
"Job": false,
"BillWithParent": false,
"Balance": 0,
"BalanceWithJobs": 0,
"CurrencyRef": {
"value": "USD",
"name": "United States Dollar"
},
"PreferredDeliveryMethod": "Print",
"IsProject": false,
"ClientEntityId": "0",
"domain": "QBO",
"sparse": false,
"Id": "3",
"SyncToken": "0",
"MetaData": {
"CreateTime": "2024-06-11T16:51:22-07:00",
"LastUpdatedTime": "2024-06-19T12:59:21-07:00"
},
"GivenName": "Grace",
"FamilyName": "Pariente",
"FullyQualifiedName": "Cool Cars",
"CompanyName": "Cool Cars",
"DisplayName": "Cool Cars",
"PrintOnCheckName": "Cool Cars",
"Active": true,
"V4IDPseudonym": "002098b664cfcba7ac42889139cc9b06d57333",
"PrimaryPhone": {
"FreeFormNumber": "(415) 555-9933"
},
"PrimaryEmailAddr": {
"Address": "Cool_Cars@intuit.com"
}
}
}

View File

@ -0,0 +1,103 @@
{
"Invoice": {
"AllowIPNPayment": false,
"AllowOnlinePayment": false,
"AllowOnlineCreditCardPayment": false,
"AllowOnlineACHPayment": false,
"domain": "QBO",
"sparse": false,
"Id": "34",
"SyncToken": "0",
"MetaData": {
"CreateTime": "2024-06-17T11:09:08-07:00",
"LastModifiedByRef": {
"value": "9341452725837119"
},
"LastUpdatedTime": "2024-06-17T11:09:08-07:00"
},
"CustomField": [],
"DocNumber": "1010",
"TxnDate": "2024-06-17",
"CurrencyRef": {
"value": "USD",
"name": "United States Dollar"
},
"LinkedTxn": [],
"Line": [
{
"Id": "1",
"LineNum": 1,
"Description": "Custom Design",
"Amount": 375,
"DetailType": "SalesItemLineDetail",
"SalesItemLineDetail": {
"ItemRef": {
"value": "4",
"name": "Design"
},
"UnitPrice": 75,
"Qty": 5,
"ItemAccountRef": {
"value": "82",
"name": "Design income"
},
"TaxCodeRef": {
"value": "NON"
}
}
},
{
"Amount": 375,
"DetailType": "SubTotalLineDetail",
"SubTotalLineDetail": {}
}
],
"TxnTaxDetail": {
"TotalTax": 0
},
"CustomerRef": {
"value": "29",
"name": "Weiskopf Consulting"
},
"CustomerMemo": {
"value": "Thank you for your business and have a great day!"
},
"BillAddr": {
"Id": "56",
"Line1": "Nicola Weiskopf",
"Line2": "Weiskopf Consulting",
"Line3": "45612 Main St.",
"Line4": "Bayshore, CA 94326",
"Lat": "INVALID",
"Long": "INVALID"
},
"ShipAddr": {
"Id": "30",
"Line1": "45612 Main St.",
"City": "Bayshore",
"CountrySubDivisionCode": "CA",
"PostalCode": "94326",
"Lat": "45.256574",
"Long": "-66.0943698"
},
"FreeFormAddress": true,
"SalesTermRef": {
"value": "3",
"name": "Net 30"
},
"DueDate": "2024-07-17",
"TotalAmt": 375,
"ApplyTaxAfterDiscount": false,
"PrintStatus": "NotSet",
"EmailStatus": "NeedToSend",
"BillEmail": {
"Address": "Consulting@intuit.com"
},
"Balance": 375,
"DeliveryInfo": {
"DeliveryType": "Email"
}
},
"time": "2015-07-24T10:48:27.082-07:00"
}

View File

@ -0,0 +1,15 @@
{
"Item":{
"Name": "Pump",
"Description": "Fountain Pump",
"UnitPrice": 15,
"QtyOnHand": 25,
"sparse": true,
"Id": "11",
"MetaData": {
"CreateTime": "2024-06-16T10:46:45-07:00",
"LastUpdatedTime": "2024-06-19T13:16:17-07:00"
}
}
}

View File

@ -0,0 +1,21 @@
Status: 200
Connection: keep-alive
Keep-Alive: timeout=5
Strict-Transport-Security: max-age=15552000
Cache-Control: no-cache, no-store
Content-Type: application/json;charset=utf-8
Server: nginx
{
"refreshToken": "AB11730384727MVrceUmO0gapXJFBT0IkpkI71FCkkWrRAVn8P",
"accessToken": "eyJlbmMiOiJBMTI4Q0JDLUhTMjU2IiwiYWxnIjoiZGlyIn0..vW8l3PbGZGjF5Nbq-ssyKw
     .mSxu2T4j4OYXa8Gr7z2OU5cqVg19MasGFllKMvT2Jo0wYrovyN6q9OZIwul1iSKI10A3ZIBT5VnsTnW6ZQG
     -rKcvmPNImZhYyCeBwzAUcnFQDPax5sWqlSHwEEviLPSH6R6Rs0wSNtszD4sEva8Z2pHmMZ4q2VNX3SAJA2R8spLhkItcULW4COdUNhZHl9fk4m7Xo66q8iBmE
     IV5269DXI-x8tCe7BdBrQbIabd5tqmD4gllS4vInK__86LLvESHDuY51eQEJt8eLRhSFIK7ZT2zoS6JKp1TfuvEg7HULkP53u12hSZsTETCfFneofOXhqEAQa8
     SgqiYV-8_I4Mm7P_TDeRX06CdPMMwH9Wrvmmmihhk8TNz6MynEn4Cpjf_iAedtyhXnELTnefTUC8_
     --w9_5FCCHyHgS45A2289mhIY1eLH_i8gjzuGKs7zYOaTvm1nQ_Jt1Z1Guy-jteNwt6_2OoMYfQaMss8AwHAZr7c
     -bhvtBY0Qi0VoqfMUdicuxMXd3HZCXUuKqXpGn5TXwJ6T6flf8Slgh2GIKUhMg1IoXMFKD3IVaPU81qyaGbuBIhwVdLywgY0guAlnYYUAHX4n_pmDf6zGxGQ0V
     RXMF2iQY1Q21OKGUqgkS8xaQajV5KERVZkp4thEurLg4EAxDdT71VxOjU5IszIRPfg8bw_X5g73pTNyF
     -3TRCJHe_FXz7P_Ee3Py_9O1Bw7MtbKxiwlSmumOOBKHAtorQhakw.nYNzTumdNDhpqLwCIkJNlw",
"expires_in": 3600,
"x_refresh_token_expires_in": 8726400
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,21 @@
Status: 200
Connection: keep-alive
Keep-Alive: timeout=5
Strict-Transport-Security: max-age=15552000
Cache-Control: no-cache, no-store
Content-Type: application/json;charset=utf-8
Server: nginx
{
"refreshToken": "AB11730389456hovpbWZFrn4st1d0qkEdnL9g3N0TNWERGeJvg",
"accessToken": "eyJlbmMiOiJBMTI4Q0JDLUhTMjU2IiwiYWxnIjoiZGlyIn0..JKD1fuk8W29Agq8x-CF3FQ
     .ioAPe43etRVWLT3GUYdF9yS_mqYqDsIkOE3Y69g7i5tj1N0mHkdxoTpfAoP59Q5PtAzndTWHjFEIF_z2mMkyuTUAJqTvrMjyRM
     -AymqFpPTly7UUs_lWZCCta5RRT8uEifeUT5VcJNT3_1nbNL85mHpXbkW-Ficovp0tR4E52BPjdrSfFbAkVQQoZnHe13rJWLgj9L
     -R63UzwlktE3lF7sgaGg5Gdsu41b8ATa0Jp2LZ4lT3QnPVcbhx8awWlwJ8qW-hElkmeBKPLju2xR1JIhw7bpDtV1BAOBkEjs7TL5oKKiUxV-slZQBhmf8cTJT
     -p6RqtOCyV-mlAL7kixc7CGWtuqwnb6j_HckdIwV8CX7Wivvt-Gnl80v0qjUQsacwighkm0KAYMoM__eTi8DM-xdvskzoLfPzblImxwKte
     -31tBg_o9v9oE6ZHr0D-yllGv58Cu5vdsFajEEo7-MTFnxpxihd8_OmXSdegGF3lLA2lMk7qlkrq6
     -Uw46rOfv4LkBo2C0OIYN1c5Wnt6zf7461BM5NerMEzeHrgjFDiTBamdWeXQfBOZAX1KJ1IoM_RJrpCParJqTx1Ia0bDn_fcT5ysTEUO9c811GWYpMhyVCq
     -qIzz4JrdIx7F7xVc4dLffVL2PHibC-cXtWcx85j1KzPOr0YEAFGoawyCH6uvMuubJlisc_BSDa_SH_gDyTtMhzeOW
     -zQi0HXveOjtQPbIa91K9ddhcilB1LSDO4Qg.fxg-wLi7mDiHgi6YTgm36Q",
"expires_in": 3600,
"x_refresh_token_expires_in": 8726345
}

View File

@ -0,0 +1,50 @@
<?php
namespace Tests\Unit\Import\Transformer\Quickbooks;
use Tests\TestCase;
use App\Import\Transformer\Quickbooks\ClientTransformer;
class ClientTransformerTest extends TestCase
{
private $customer_data;
private $tranformed_data;
private $transformer;
protected function setUp(): void
{
parent::setUp();
// Mock the company object
$company = (new \App\Factory\CompanyFactory)->create(1234);
// Read the JSON string from a file and decode into an associative array
$this->customer_data = json_decode( file_get_contents( app_path('/../tests/Mock/Quickbooks/Data/customer.json') ), true);
$this->transformer = new ClientTransformer($company);
$this->transformed_data = $this->transformer->transform($this->customer_data['Customer']);
}
public function testClassExists()
{
$this->assertInstanceOf(ClientTransformer::class, $this->transformer);
}
public function testTransformReturnsArray()
{
$this->assertIsArray($this->transformed_data);
}
public function testTransformHasNameProperty()
{
$this->assertArrayHasKey('name', $this->transformed_data);
$this->assertEquals($this->customer_data['Customer']['CompanyName'], $this->transformed_data['name']);
}
public function testTransformHasContactsProperty()
{
$this->assertArrayHasKey('contacts', $this->transformed_data);
$this->assertIsArray($this->transformed_data['contacts']);
$this->assertArrayHasKey(0, $this->transformed_data['contacts']);
$this->assertArrayHasKey('email', $this->transformed_data['contacts'][0]);
$this->assertEquals($this->customer_data['Customer']['PrimaryEmailAddr']['Address'], $this->transformed_data['contacts'][0]['email']);
}
}

View File

@ -0,0 +1,75 @@
<?php
namespace Tests\Unit\Import\Transformer\Quickbooks;
use Tests\TestCase;
use Tests\MockAccountData;
use Illuminate\Support\Facades\Auth;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use App\Import\Transformer\Quickbooks\InvoiceTransformer;
class InvoiceTransformerTest extends TestCase
{
use MockAccountData;
use DatabaseTransactions;
private $invoiceData;
private $tranformedData;
private $transformer;
protected function setUp(): void
{
parent::setUp();
$this->makeTestData();
$this->withoutExceptionHandling();
Auth::setUser($this->user);
// Read the JSON string from a file and decode into an associative array
$this->invoiceData = json_decode( file_get_contents( app_path('/../tests/Mock/Quickbooks/Data/invoice.json') ), true);
$this->transformer = new InvoiceTransformer($this->company);
$this->transformedData = $this->transformer->transform($this->invoiceData['Invoice']);
}
public function testIsInstanceOf()
{
$this->assertInstanceOf(InvoiceTransformer::class, $this->transformer);
}
public function testTransformReturnsArray()
{
$this->assertIsArray($this->transformedData);
}
public function testTransformContainsNumber()
{
$this->assertArrayHasKey('number', $this->transformedData);
$this->assertEquals($this->invoiceData['Invoice']['DocNumber'], $this->transformedData['number']);
}
public function testTransformContainsDueDate()
{
$this->assertArrayHasKey('due_date', $this->transformedData);
$this->assertEquals(strtotime($this->invoiceData['Invoice']['DueDate']), strtotime($this->transformedData['due_date']));
}
public function testTransformContainsAmount()
{
$this->assertArrayHasKey('amount', $this->transformedData);
$this->assertIsFloat($this->transformedData['amount']);
$this->assertEquals($this->invoiceData['Invoice']['TotalAmt'], $this->transformedData['amount']);
}
public function testTransformContainsLineItems()
{
$this->assertArrayHasKey('line_items', $this->transformedData);
$this->assertNotNull($this->transformedData['line_items']);
$this->assertEquals( count($this->invoiceData['Invoice']["Line"]) - 1, count($this->transformedData['line_items']) );
}
public function testTransformHasClient()
{
$this->assertArrayHasKey('client', $this->transformedData);
$this->assertArrayHasKey('contacts', $this->transformedData['client']);
$this->assertEquals($this->invoiceData['Invoice']['BillEmail']['Address'], $this->transformedData['client']['contacts'][0]['email']);
}
}

View File

@ -0,0 +1,44 @@
<?php
namespace Tests\Unit\Import\Transformer\Quickbooks;
use Tests\TestCase;
use App\Import\Transformer\Quickbooks\ProductTransformer;
class ProductTransformerTest extends TestCase
{
private $product_data;
private $tranformed_data;
private $transformer;
protected function setUp(): void
{
parent::setUp();
// Mock the company object
$company = (new \App\Factory\CompanyFactory)->create(1234);
// Read the JSON string from a file and decode into an associative array
$this->product_data = json_decode( file_get_contents( app_path('/../tests/Mock/Quickbooks/Data/item.json') ), true);
$this->transformer = new ProductTransformer($company);
$this->transformed_data = $this->transformer->transform($this->product_data['Item']);
}
public function testClassExists()
{
$this->assertInstanceOf(ProductTransformer::class, $this->transformer);
}
public function testTransformReturnsArray()
{
$this->assertIsArray($this->transformed_data);
}
public function testTransformHasProperties()
{
$this->assertArrayHasKey('product_key', $this->transformed_data);
$this->assertArrayHasKey('price', $this->transformed_data);
$this->assertTrue(is_numeric($this->transformed_data['price']));
$this->assertEquals(15, (int) $this->transformed_data['price'] );
$this->assertEquals((int) $this->product_data['Item']['QtyOnHand'], $this->transformed_data['quantity']);
}
}

View File

@ -0,0 +1,47 @@
<?php
// tests/Unit/IntuitSDKWrapperTest.php
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;
class SdkWrapperTest extends TestCase
{
protected $sdk;
protected $sdkMock;
protected function setUp(): void
{
parent::setUp();
$this->sdkMock = Mockery::mock(sdtClass::class);
$this->sdk = new QuickbookSDK($this->sdkMock);
}
function testIsInstanceOf() {
$this->assertInstanceOf(SdkInterface::class, $this->sdk);
}
function testMethodFetchRecords() {
$data = json_decode(
file_get_contents(base_path('tests/Mock/Quickbooks/Data/customers.json')),true
);
$count = count($data);
$this->sdkMock->shouldReceive('Query')->andReturnUsing(function($val) use ($count, $data) {
if(stristr($val, 'count')) {
return $count;
}
return Arr::take($data,4);
});
$this->assertEquals($count, $this->sdk->totalRecords('Customer'));
$this->assertEquals(4, count($this->sdk->fetchRecords('Customer', 4)));
}
}

View File

@ -0,0 +1,54 @@
<?php
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;
class ServiceTest extends TestCase
{
protected $service;
protected function setUp(): void
{
parent::setUp();
// Inject the mock into the IntuitSDKservice instance
$this->service = Mockery::mock( new QuickbooksService(Mockery::mock(QuickbooksInterface::class)))->shouldAllowMockingProtectedMethods();
}
protected function tearDown(): void
{
Mockery::close();
parent::tearDown();
}
public function testTotalRecords()
{
$entity = 'Customer';
$count = 10;
$this->service->shouldReceive('totalRecords')
->with($entity)
->andReturn($count);
$result = $this->service->totalRecords($entity);
$this->assertEquals($count, $result);
}
public function testHasFetchRecords()
{
$entity = 'Customer';
$count = 10;
$this->service->shouldReceive('fetchRecords')
->with($entity, $count)
->andReturn(collect());
$result = $this->service->fetchCustomers($count);
$this->assertInstanceOf(Collection::class, $result);
}
}