mirror of
https://github.com/invoiceninja/invoiceninja.git
synced 2025-06-23 20:00:33 -04:00
Merge branch 'feature/import-quickbooks' into v5-develop
Signed-off-by: Kendall Arneaud <kendall.arneaud@gmail.com>
This commit is contained in:
commit
7c1f892ec2
196
app/Http/Controllers/ImportQuickbooksController.php
Normal file
196
app/Http/Controllers/ImportQuickbooksController.php
Normal file
@ -0,0 +1,196 @@
|
|||||||
|
<?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()) {
|
||||||
|
// If validation fails, redirect back with errors and input
|
||||||
|
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) {
|
||||||
|
|
||||||
|
$realmId = $request->query('realmId');
|
||||||
|
$tokens = $this->service->getOAuth()->accessToken($request->query('code'), $realmId);
|
||||||
|
$company = $request->input('company');
|
||||||
|
Cache::put($company['company_key'], $tokens['access_token'], $tokens['access_token_expires']);
|
||||||
|
// TODO: save refresh token and realmId in company DB
|
||||||
|
|
||||||
|
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, 90);
|
||||||
|
|
||||||
|
return redirect()->to($authorizationUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function preimport(Request $request)
|
||||||
|
{
|
||||||
|
// Check for authorization otherwise
|
||||||
|
// Create a reference
|
||||||
|
$hash = Str::random(32);
|
||||||
|
$data = [
|
||||||
|
'hash' => $hash,
|
||||||
|
'type' => $request->input('import_type', 'client'),
|
||||||
|
'max' => $request->input('max', 100)
|
||||||
|
];
|
||||||
|
$this->getData($data);
|
||||||
|
|
||||||
|
return $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"], $data['max']);
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
$this->preimport($request);
|
||||||
|
/** @var \App\Models\User $user */
|
||||||
|
$user = auth()->user();
|
||||||
|
if (Ninja::isHosted()) {
|
||||||
|
QuickbooksIngest::dispatch($request->all(), $user->company() );
|
||||||
|
} else {
|
||||||
|
QuickbooksIngest::dispatch($request->all(), $user->company() );
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json(['message' => 'Processing'], 200);
|
||||||
|
}
|
||||||
|
}
|
226
app/Import/Providers/Quickbooks.php
Normal file
226
app/Import/Providers/Quickbooks.php
Normal file
@ -0,0 +1,226 @@
|
|||||||
|
<?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\Factory\ProductFactory;
|
||||||
|
use App\Factory\ClientFactory;
|
||||||
|
use App\Factory\InvoiceFactory;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
use App\Http\Requests\Client\StoreClientRequest;
|
||||||
|
use App\Http\Requests\Product\StoreProductRequest;
|
||||||
|
use App\Http\Requests\Invoice\StoreInvoiceRequest;
|
||||||
|
use App\Import\Transformer\Quickbooks\ClientTransformer;
|
||||||
|
use App\Import\Transformer\Quickbooks\InvoiceTransformer;
|
||||||
|
use App\Import\Transformer\Quickbooks\ProductTransformer;
|
||||||
|
use App\Repositories\ClientRepository;
|
||||||
|
use App\Repositories\InvoiceRepository;
|
||||||
|
use App\Repositories\ProductRepository;
|
||||||
|
|
||||||
|
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()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
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'];
|
||||||
|
|
||||||
|
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 {
|
||||||
|
$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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
90
app/Import/Transformer/Quickbooks/ClientTransformer.php
Normal file
90
app/Import/Transformer/Quickbooks/ClientTransformer.php
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
<?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\BaseTransformer;
|
||||||
|
use App\Models\Client as Model;
|
||||||
|
use App\Models\ClientContact;
|
||||||
|
use App\Import\ImportException;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use Illuminate\Support\Arr;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class ClientTransformer.
|
||||||
|
*/
|
||||||
|
class ClientTransformer extends BaseTransformer
|
||||||
|
{
|
||||||
|
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'
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach($this->fillable as $key => $field) {
|
||||||
|
$transformed_data[$key] = method_exists($this, $method = sprintf("get%s", str_replace(".","",$field)) )? call_user_func([$this, $method],$data,$field) : $this->getString($data, $field);
|
||||||
|
}
|
||||||
|
|
||||||
|
$transformed_data = (new Model)->fillable(array_keys($this->fillable))->fill($transformed_data);
|
||||||
|
$transformed_data->contacts[0] = $this->getContacts($data)->toArray()+['company_id' => $this->company->id ];
|
||||||
|
return $transformed_data->toArray() + ['company_id' => $this->company->id ] ;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getString($data, $field)
|
||||||
|
{
|
||||||
|
return Arr::get($data, $field);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
169
app/Import/Transformer/Quickbooks/InvoiceTransformer.php
Normal file
169
app/Import/Transformer/Quickbooks/InvoiceTransformer.php
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
<?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\ClientTransformer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class InvoiceTransformer.
|
||||||
|
*/
|
||||||
|
class InvoiceTransformer extends BaseTransformer
|
||||||
|
{
|
||||||
|
|
||||||
|
|
||||||
|
private $fillable = [
|
||||||
|
'amount' => "TotalAmt",
|
||||||
|
'line_items' => "Line",
|
||||||
|
'due_date' => "DueDate",
|
||||||
|
'partial' => "Deposit",
|
||||||
|
'balance' => "Balance",
|
||||||
|
'comments' => "CustomerMemo",
|
||||||
|
'number' => "DocNumber",
|
||||||
|
'created_at' => "CreateTime",
|
||||||
|
'updated_at' => "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 (new Model)->fillable(array_keys($this->fillable))->fill($transformed)->toArray() + $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'),
|
||||||
|
'quantity' => $this->getString($item,'SalesItemLineDetail.Qty'),
|
||||||
|
'unit_price' =>$this->getString($item,'SalesItemLineDetail.UnitPrice'),
|
||||||
|
'amount' => $this->getString($item,'Amount')
|
||||||
|
];
|
||||||
|
}, 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 =
|
||||||
|
[
|
||||||
|
"CompanyName" => $has_company? $bill_address->Line2 : $bill_address->Line1,
|
||||||
|
"BillAddr" => array_combine(['City','CountrySubDivisionCode','PostalCode'], $address) + ['Line1' => $has_company? $bill_address->Line3 : $bill_address->Line2 ],
|
||||||
|
"ShipAddr" => $ship_address
|
||||||
|
] + $customer + ['PrimaryEmailAddr' => ['Address' => $this->getString($data, 'BillEmail.Address') ]];
|
||||||
|
$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 getString($data,$field) {
|
||||||
|
return Arr::get($data,$field);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 getCreateTime($data)
|
||||||
|
{
|
||||||
|
return $this->parseDateOrNull($data['MetaData'], 'CreateTime');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getLastUpdatedTime($data)
|
||||||
|
{
|
||||||
|
return $this->parseDateOrNull($data['MetaData'],'LastUpdatedTime');
|
||||||
|
}
|
||||||
|
}
|
88
app/Import/Transformer/Quickbooks/ProductTransformer.php
Normal file
88
app/Import/Transformer/Quickbooks/ProductTransformer.php
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
<?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\BaseTransformer;
|
||||||
|
use App\Models\Product as Model;
|
||||||
|
use App\Import\ImportException;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use Illuminate\Support\Arr;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class ProductTransformer.
|
||||||
|
*/
|
||||||
|
class ProductTransformer extends BaseTransformer
|
||||||
|
{
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'product_key' => 'Name',
|
||||||
|
'notes' => 'Description',
|
||||||
|
'cost' => 'PurchaseCost',
|
||||||
|
'price' => 'UnitPrice',
|
||||||
|
'quantity' => 'QtyOnHand',
|
||||||
|
'in_stock_quantity' => 'QtyOnHand',
|
||||||
|
'created_at' => 'CreateTime',
|
||||||
|
'updated_at' => 'LastUpdatedTime',
|
||||||
|
];
|
||||||
|
/**
|
||||||
|
* Transforms the JSON data into a ProductModel object.
|
||||||
|
*
|
||||||
|
* @param array $data
|
||||||
|
* @return ProductModel
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* Transforms a Customer array into a Product model.
|
||||||
|
*
|
||||||
|
* @param array $data
|
||||||
|
* @return array|bool
|
||||||
|
*/
|
||||||
|
public function transform($data)
|
||||||
|
{
|
||||||
|
$transformed_data = [];
|
||||||
|
foreach($this->fillable as $key => $field) {
|
||||||
|
$transformed_data[$key] = method_exists($this, $method = sprintf("get%s", str_replace(".","",$field)) )? call_user_func([$this, $method],$data,$field) : $this->getString($data, $field);
|
||||||
|
}
|
||||||
|
|
||||||
|
$transformed_data = (new Model)->fillable(array_keys($this->fillable))->fill($transformed_data);
|
||||||
|
|
||||||
|
return $transformed_data->toArray() + ['company_id' => $this->company->id ] ;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getString($data, $field)
|
||||||
|
{
|
||||||
|
return Arr::get($data, $field);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCreateTime($data, $field = null)
|
||||||
|
{
|
||||||
|
return $this->parseDateOrNull($data['MetaData'], 'CreateTime');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getLastUpdatedTime($data, $field = null)
|
||||||
|
{
|
||||||
|
return $this->parseDateOrNull($data['MetaData'],'LastUpdatedTime');
|
||||||
|
}
|
||||||
|
}
|
42
app/Jobs/Import/QuickbooksIngest.php
Normal file
42
app/Jobs/Import/QuickbooksIngest.php
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
<?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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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($this->request, $this->company);
|
||||||
|
foreach (['client', 'product', 'invoice', 'payment'] as $entity) {
|
||||||
|
$engine->import($entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
$engine->finalizeImport();
|
||||||
|
}
|
||||||
|
}
|
102
app/Providers/QuickbooksServiceProvider.php
Normal file
102
app/Providers/QuickbooksServiceProvider.php
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Providers;
|
||||||
|
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Support\Facades\Route;
|
||||||
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
use QuickBooksOnline\API\DataService\DataService;
|
||||||
|
use App\Http\Controllers\ImportQuickbooksController;
|
||||||
|
use App\Services\Import\Quickbooks\Service as QuickbooksService;
|
||||||
|
use App\Services\Import\Quickbooks\Auth as QuickbooksAuthService;
|
||||||
|
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) {
|
||||||
|
// TODO: Load tokens from Cache and DB?
|
||||||
|
$sdk = DataService::Configure(config('services.quickbooks.settings') + ['state' => Str::random(12)]);
|
||||||
|
if(env('APP_DEBUG')) {
|
||||||
|
$sdk->setLogLocation(storage_path("logs/quickbooks.log"));
|
||||||
|
$sdk->enableLog();
|
||||||
|
}
|
||||||
|
|
||||||
|
$sdk->setMinorVersion("73");
|
||||||
|
$sdk->throwExceptionOnError(true);
|
||||||
|
|
||||||
|
return new QuickbooksSDKWrapper($sdk);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Register SDKWrapper with DataService dependency
|
||||||
|
$this->app->singleton(QuickbooksService::class, function ($app) {
|
||||||
|
return new QuickbooksService($app->make(QuickbooksInterface::class));
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->app->singleton(QuickbooksAuthService::class, function ($app) {
|
||||||
|
return new QuickbooksAuthService($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::middleware('api')
|
||||||
|
->namespace($this->app->getNamespace() . 'Http\Controllers')
|
||||||
|
->group(function () {
|
||||||
|
Route::post('import/quickbooks', [ImportQuickbooksController::class, 'import'])->name('import.quickbooks');
|
||||||
|
//Route::post('import/quickbooks/preimport', [ImportQuickbooksController::class, 'preimport'])->name('import.quickbooks.preimport');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
11
app/Repositories/Import/Quickbooks/CustomerRepository.php
Normal file
11
app/Repositories/Import/Quickbooks/CustomerRepository.php
Normal 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";
|
||||||
|
}
|
38
app/Repositories/Import/Quickbooks/Repository.php
Normal file
38
app/Repositories/Import/Quickbooks/Repository.php
Normal 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,42 @@
|
|||||||
|
<?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 transformInvoices(array $items): Collection
|
||||||
|
{
|
||||||
|
return $this->transformation($items, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function transformCustomers(array $items): Collection
|
||||||
|
{
|
||||||
|
return $this->transformation($items, [
|
||||||
|
'CompanyName',
|
||||||
|
'PrimaryPhone',
|
||||||
|
'BillAddr',
|
||||||
|
'ShipAddr',
|
||||||
|
'Notes',
|
||||||
|
'GivenName',
|
||||||
|
'FamilyName',
|
||||||
|
'PrimaryEmailAddr',
|
||||||
|
'CurrencyRef',
|
||||||
|
'MetaData'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function transformation(array $items, array $keys) : Collection
|
||||||
|
{
|
||||||
|
return collect($items)->select($keys);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
37
app/Services/Import/Quickbooks/Auth.php
Normal file
37
app/Services/Import/Quickbooks/Auth.php
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
namespace App\Services\Import\Quickbooks;
|
||||||
|
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
14
app/Services/Import/Quickbooks/Contracts/SdkInterface.php
Normal file
14
app/Services/Import/Quickbooks/Contracts/SdkInterface.php
Normal 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;
|
||||||
|
}
|
107
app/Services/Import/Quickbooks/SdkWrapper.php
Normal file
107
app/Services/Import/Quickbooks/SdkWrapper.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
73
app/Services/Import/Quickbooks/Service.php
Normal file
73
app/Services/Import/Quickbooks/Service.php
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
<?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
|
||||||
|
{
|
||||||
|
// TODO: Cache token and
|
||||||
|
$token = $this->sdk->getAccessToken();
|
||||||
|
$access_token = $token->getAccessToken();
|
||||||
|
$refresh_token = $token->getRefreshToken();
|
||||||
|
$access_token_expires = $token->getAccessTokenExpiresAt();
|
||||||
|
$refresh_token_expires = $token->getRefreshTokenExpiresAt();
|
||||||
|
//TODO: Cache token object. Update $sdk instance?
|
||||||
|
return compact('access_token', 'refresh_token','access_token_expires', 'refresh_token_expires');
|
||||||
|
}
|
||||||
|
|
||||||
|
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->transformer->transform($this->fetchRecords( 'Invoice', $max), 'Invoice');
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
@ -62,6 +62,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",
|
||||||
@ -86,6 +87,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",
|
||||||
"razorpay/razorpay": "2.*",
|
"razorpay/razorpay": "2.*",
|
||||||
"sentry/sentry-laravel": "^4",
|
"sentry/sentry-laravel": "^4",
|
||||||
"setasign/fpdf": "^1.8",
|
"setasign/fpdf": "^1.8",
|
||||||
|
59
composer.lock
generated
59
composer.lock
generated
@ -9834,6 +9834,65 @@
|
|||||||
},
|
},
|
||||||
"time": "2023-12-15T10:58:53+00:00"
|
"time": "2023-12-15T10:58:53+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "quickbooks/v3-php-sdk",
|
||||||
|
"version": "v6.1.3",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/intuit/QuickBooks-V3-PHP-SDK.git",
|
||||||
|
"reference": "2e7be89a9b2e846ec8c8fdceb4c9bf102317f3a2"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/intuit/QuickBooks-V3-PHP-SDK/zipball/2e7be89a9b2e846ec8c8fdceb4c9bf102317f3a2",
|
||||||
|
"reference": "2e7be89a9b2e846ec8c8fdceb4c9bf102317f3a2",
|
||||||
|
"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/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"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": {
|
||||||
|
"issues": "https://github.com/intuit/QuickBooks-V3-PHP-SDK/issues",
|
||||||
|
"source": "https://github.com/intuit/QuickBooks-V3-PHP-SDK/tree/v6.1.3"
|
||||||
|
},
|
||||||
|
"time": "2024-05-28T11:13:18+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "ralouphie/getallheaders",
|
"name": "ralouphie/getallheaders",
|
||||||
"version": "3.0.3",
|
"version": "3.0.3",
|
||||||
|
@ -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(),
|
||||||
|
|
||||||
];
|
];
|
||||||
|
@ -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');
|
||||||
|
@ -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;
|
||||||
|
@ -0,0 +1,135 @@
|
|||||||
|
<?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();
|
||||||
|
// $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);
|
||||||
|
// }
|
||||||
|
// );
|
||||||
|
// $this->setUpTestData('customers');
|
||||||
|
// // Perform the test
|
||||||
|
// $response = $this->withHeaders([
|
||||||
|
// 'X-API-TOKEN' => $this->token,
|
||||||
|
// ])->post('/api/v1/import/quickbooks/preimport',[
|
||||||
|
// 'import_type' => 'client'
|
||||||
|
// ]);
|
||||||
|
// $response->assertStatus(200);
|
||||||
|
// $response = json_decode( $response->getContent());
|
||||||
|
// $this->assertNotNull($response->hash);
|
||||||
|
// $hash = $response->hash;
|
||||||
|
// $response = $this->withHeaders([
|
||||||
|
// 'X-API-TOKEN' => $this->token,
|
||||||
|
// ])->post('/api/v1/import/quickbooks',[
|
||||||
|
// 'import_type' => 'client',
|
||||||
|
// 'hash' => $response->hash
|
||||||
|
// ]);
|
||||||
|
// $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
|
||||||
|
);
|
||||||
|
$count = count($data);
|
||||||
|
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
}
|
139
tests/Feature/Import/Quickbooks/QuickbooksTest.php
Normal file
139
tests/Feature/Import/Quickbooks/QuickbooksTest.php
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
1602
tests/Feature/Import/customers.json
Normal file
1602
tests/Feature/Import/customers.json
Normal file
File diff suppressed because it is too large
Load Diff
4101
tests/Feature/Import/invoices.json
Normal file
4101
tests/Feature/Import/invoices.json
Normal file
File diff suppressed because it is too large
Load Diff
204
tests/Feature/Import/items.json
Normal file
204
tests/Feature/Import/items.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
53
tests/Feature/Jobs/Import/QuickbooksIngestTest.php
Normal file
53
tests/Feature/Jobs/Import/QuickbooksIngestTest.php
Normal 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_type' => 'quickbooks',
|
||||||
|
], $this->company )->handle();
|
||||||
|
$this->assertTrue(Client::withTrashed()->where(['company_id' => $this->company->id, 'name' => "Freeman Sporting Goods"])->exists());
|
||||||
|
}
|
||||||
|
}
|
@ -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']);
|
||||||
|
}
|
||||||
|
}
|
48
tests/Mock/Quickbooks/Data/customer.json
Normal file
48
tests/Mock/Quickbooks/Data/customer.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
103
tests/Mock/Quickbooks/Data/invoice.json
Normal file
103
tests/Mock/Quickbooks/Data/invoice.json
Normal 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"
|
||||||
|
}
|
15
tests/Mock/Quickbooks/Data/item.json
Normal file
15
tests/Mock/Quickbooks/Data/item.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
1617
tests/Mock/Quickbooks/Http/Response/200-cutomer-response.txt
Normal file
1617
tests/Mock/Quickbooks/Http/Response/200-cutomer-response.txt
Normal file
File diff suppressed because it is too large
Load Diff
4114
tests/Mock/Quickbooks/Http/Response/200-invoice-response.txt
Normal file
4114
tests/Mock/Quickbooks/Http/Response/200-invoice-response.txt
Normal file
File diff suppressed because it is too large
Load Diff
@ -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
|
||||||
|
}
|
@ -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']);
|
||||||
|
}
|
||||||
|
}
|
@ -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']);
|
||||||
|
}
|
||||||
|
}
|
@ -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']);
|
||||||
|
}
|
||||||
|
}
|
47
tests/Unit/Services/Import/Quickbooks/SdkWrapperTest.php
Normal file
47
tests/Unit/Services/Import/Quickbooks/SdkWrapperTest.php
Normal 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)));
|
||||||
|
}
|
||||||
|
}
|
54
tests/Unit/Services/Import/Quickbooks/ServiceTest.php
Normal file
54
tests/Unit/Services/Import/Quickbooks/ServiceTest.php
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user