Merge branch 'feature/import-quickbooks' into v5-develop

Signed-off-by: Kendall Arneaud <kendall.arneaud@gmail.com>
This commit is contained in:
Kendall Arneaud 2024-08-02 10:02:45 -04:00 committed by GitHub
commit 7c1f892ec2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
39 changed files with 13805 additions and 3 deletions

View 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);
}
}

View 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;
}
}

View 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);
}
}

View 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');
}
}

View 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');
}
}

View 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();
}
}

View 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');
});
}
}

View File

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

View File

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

View File

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

View File

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

View 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();
}
}

View File

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

View File

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

View File

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

View File

@ -62,6 +62,7 @@
"imdhemy/laravel-purchases": "^1.7",
"intervention/image": "^2.5",
"invoiceninja/einvoice": "dev-main",
"invoiceninja/inspector": "^3.0",
"invoiceninja/ubl_invoice": "^2",
"josemmo/facturae-php": "^1.7",
"laracasts/presenter": "^0.2.1",
@ -86,6 +87,7 @@
"predis/predis": "^2",
"psr/http-message": "^1.0",
"pusher/pusher-php-server": "^7.2",
"quickbooks/v3-php-sdk": "^6.1",
"razorpay/razorpay": "2.*",
"sentry/sentry-laravel": "^4",
"setasign/fpdf": "^1.8",

59
composer.lock generated
View File

@ -9834,6 +9834,65 @@
},
"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",
"version": "3.0.3",

View File

@ -200,7 +200,8 @@ return [
App\Providers\MultiDBProvider::class,
App\Providers\ClientPortalServiceProvider::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([
'Collector' => Turbo124\Beacon\CollectorFacade::class,
'CustomMessage' => App\Utils\ClientPortal\CustomMessage\CustomMessageFacade::class,
'Redis' => Illuminate\Support\Facades\Redis::class,
'Redis' => Illuminate\Support\Facades\Redis::class
])->toArray(),
];

View File

@ -57,6 +57,7 @@ use App\Http\Controllers\SystemLogController;
use App\Http\Controllers\TwoFactorController;
use App\Http\Controllers\Auth\LoginController;
use App\Http\Controllers\ImportJsonController;
use App\Http\Controllers\ImportQuickbooksController;
use App\Http\Controllers\SelfUpdateController;
use App\Http\Controllers\TaskStatusController;
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_json', [ImportJsonController::class, 'import'])->name('import.import_json');
Route::post('preimport', [ImportController::class, 'preimport'])->name('import.preimport');
;
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}/{action}', [InvoiceController::class, 'action'])->name('invoices.action');

View File

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

View File

@ -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;
}
}

View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,54 @@
<?php
namespace Tests\Unit\Services\Import\Quickbooks;
use Mockery;
use Tests\TestCase;
use Illuminate\Support\Collection;
use App\Services\Import\Quickbooks\Service as QuickbooksService;
use App\Services\Import\Quickbooks\Contracts\SdkInterface as QuickbooksInterface;
class ServiceTest extends TestCase
{
protected $service;
protected function setUp(): void
{
parent::setUp();
// Inject the mock into the IntuitSDKservice instance
$this->service = Mockery::mock( new QuickbooksService(Mockery::mock(QuickbooksInterface::class)))->shouldAllowMockingProtectedMethods();
}
protected function tearDown(): void
{
Mockery::close();
parent::tearDown();
}
public function testTotalRecords()
{
$entity = 'Customer';
$count = 10;
$this->service->shouldReceive('totalRecords')
->with($entity)
->andReturn($count);
$result = $this->service->totalRecords($entity);
$this->assertEquals($count, $result);
}
public function testHasFetchRecords()
{
$entity = 'Customer';
$count = 10;
$this->service->shouldReceive('fetchRecords')
->with($entity, $count)
->andReturn(collect());
$result = $this->service->fetchCustomers($count);
$this->assertInstanceOf(Collection::class, $result);
}
}