mirror of
https://github.com/invoiceninja/invoiceninja.git
synced 2025-07-07 15:54:31 -04:00
Merge pull request #9917 from M-E-Development-Design/feature/import-quickbooks
Feature/import quickbooks
This commit is contained in:
commit
37822d7a0d
63
app/Factory/QuickbooksSDKFactory.php
Normal file
63
app/Factory/QuickbooksSDKFactory.php
Normal file
@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace App\Factory;
|
||||
|
||||
use App\Libraries\MultiDB;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use QuickBooksOnline\API\DataService\DataService;
|
||||
use App\Services\Import\Quickbooks\Repositories\CompanyTokensRepository;
|
||||
|
||||
|
||||
class QuickbooksSDKFactory
|
||||
{
|
||||
public static function create()
|
||||
{
|
||||
$tokens = [];
|
||||
// Ensure the user is authenticated
|
||||
if(($user = Auth::user()))
|
||||
{
|
||||
$company = $user->company();
|
||||
|
||||
$token_store = (new CompanyTokensRepository($company->company_key));
|
||||
$tokens = array_filter($token_store->get());
|
||||
if(!empty($tokens)) {
|
||||
$keys = ['refreshTokenKey','QBORealmID'];
|
||||
if(array_key_exists('access_token', $tokens)) {
|
||||
$keys = array_merge(['accessTokenKey'] ,$keys);
|
||||
}
|
||||
|
||||
$tokens = array_combine($keys, array_values($tokens));
|
||||
}
|
||||
}
|
||||
|
||||
$config = $tokens + config('services.quickbooks.settings') + [
|
||||
'state' => Str::random(12)
|
||||
];
|
||||
$sdk = DataService::Configure($config);
|
||||
if (env('APP_DEBUG')) {
|
||||
$sdk->setLogLocation(storage_path("logs/quickbooks.log"));
|
||||
$sdk->enableLog();
|
||||
}
|
||||
|
||||
$sdk->setMinorVersion("73");
|
||||
$sdk->throwExceptionOnError(true);
|
||||
if(array_key_exists('refreshTokenKey', $config) && !array_key_exists('accessTokenKey', $config))
|
||||
{
|
||||
$tokens = ($sdk->getOAuth2LoginHelper())->refreshToken();
|
||||
$sdk = $sdk->updateOAuth2Token($tokens);
|
||||
$tokens = ($sdk->getOAuth2LoginHelper())->getAccessToken();
|
||||
$access_token = $tokens->getAccessToken();
|
||||
$realm = $tokens->getRealmID();
|
||||
$refresh_token = $tokens->getRefreshToken();
|
||||
$access_token_expires = $tokens->getAccessTokenExpiresAt();
|
||||
$refresh_token_expires = $tokens->getRefreshTokenExpiresAt();
|
||||
$tokens = compact('access_token', 'refresh_token','access_token_expires', 'refresh_token_expires','realm');
|
||||
$token_store->save($tokens);
|
||||
}
|
||||
|
||||
return $sdk;
|
||||
}
|
||||
}
|
199
app/Http/Controllers/ImportQuickbooksController.php
Normal file
199
app/Http/Controllers/ImportQuickbooksController.php
Normal file
@ -0,0 +1,199 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use \Closure;
|
||||
use App\Utils\Ninja;
|
||||
use App\Models\Company;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use App\Utils\Traits\MakesHash;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use App\Jobs\Import\QuickbooksIngest;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use App\Services\Import\Quickbooks\Service as QuickbooksService;
|
||||
|
||||
class ImportQuickbooksController extends BaseController
|
||||
{
|
||||
protected QuickbooksService $service;
|
||||
private $import_entities = [
|
||||
'client' => 'Customer',
|
||||
'invoice' => 'Invoice',
|
||||
'product' => 'Item',
|
||||
'payment' => 'Payment'
|
||||
];
|
||||
|
||||
public function __construct(QuickbooksService $service) {
|
||||
parent::__construct();
|
||||
|
||||
$this->service = $service;
|
||||
$this->middleware(
|
||||
function (Request $request, Closure $next) {
|
||||
|
||||
// Check for the required query parameters
|
||||
if (!$request->has(['code', 'state', 'realmId'])) {
|
||||
return abort(400,'Unauthorized');
|
||||
}
|
||||
|
||||
$rules = [
|
||||
'state' => [
|
||||
'required',
|
||||
'valid' => function ($attribute, $value, $fail) {
|
||||
if (!Cache::has($value)) {
|
||||
$fail('The state is invalid.');
|
||||
}
|
||||
},
|
||||
]
|
||||
];
|
||||
// Custom error messages
|
||||
$messages = [
|
||||
'state.required' => 'The state is required.',
|
||||
'state.valid' => 'state token not valid'
|
||||
];
|
||||
// Perform the validation
|
||||
$validator = Validator::make($request->all(), $rules, $messages);
|
||||
if ($validator->fails()) {
|
||||
// If validation fails, redirect back with errors and input
|
||||
return redirect('/')
|
||||
->withErrors($validator)
|
||||
->withInput();
|
||||
}
|
||||
|
||||
$token = Cache::pull($request->state);
|
||||
$request->merge(['company' => Cache::get($token) ]);
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
)->only('onAuthorized');
|
||||
$this->middleware(
|
||||
function ( Request $request, Closure $next) {
|
||||
$rules = [
|
||||
'token' => [
|
||||
'required',
|
||||
'valid' => function ($attribute, $value, $fail) {
|
||||
if (!Cache::has($value) || (!Company::where('company_key', (Cache::get($value))['company_key'])->exists() )) {
|
||||
$fail('The company is invalid.');
|
||||
}
|
||||
},
|
||||
]
|
||||
];
|
||||
// Custom error messages
|
||||
$messages = [
|
||||
'token.required' => 'The token is required.',
|
||||
'token.valid' => 'Token note valid!'
|
||||
];
|
||||
// Perform the validation
|
||||
$validator = Validator::make(['token' => $request->token ], $rules, $messages);
|
||||
if ($validator->fails()) {
|
||||
return redirect()
|
||||
->back()
|
||||
->withErrors($validator)
|
||||
->withInput();
|
||||
}
|
||||
|
||||
//If validation passes, proceed to the next middleware/controller
|
||||
return $next($request);
|
||||
}
|
||||
)->only('authorizeQuickbooks');
|
||||
}
|
||||
|
||||
public function onAuthorized(Request $request)
|
||||
{
|
||||
$realm = $request->query('realmId');
|
||||
$company_key = $request->input('company.company_key');
|
||||
$company_id = $request->input('company.id');
|
||||
$tokens = ($auth_service = $this->service->getOAuth())->accessToken($request->query('code'), $realm);
|
||||
$auth_service->saveTokens($company_key, ['realm' => $realm] + $tokens);
|
||||
|
||||
return response(200);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function authorizeQuickbooks(Request $request)
|
||||
{
|
||||
$token = $request->token;
|
||||
$auth = $this->service->getOAuth();
|
||||
$authorizationUrl = $auth->getAuthorizationUrl();
|
||||
$state = $auth->getState();
|
||||
|
||||
Cache::put($state, $token, 190);
|
||||
|
||||
return redirect()->to($authorizationUrl);
|
||||
}
|
||||
|
||||
public function preimport(string $type, string $hash)
|
||||
{
|
||||
// Check for authorization otherwise
|
||||
// Create a reference
|
||||
$data = [
|
||||
'hash' => $hash,
|
||||
'type' => $type
|
||||
];
|
||||
$this->getData($data);
|
||||
}
|
||||
|
||||
protected function getData($data) {
|
||||
|
||||
$entity = $this->import_entities[$data['type']];
|
||||
$cache_name = "{$data['hash']}-{$data['type']}";
|
||||
// TODO: Get or put cache or DB?
|
||||
if(! Cache::has($cache_name) )
|
||||
{
|
||||
$contents = call_user_func([$this->service, "fetch{$entity}s"]);
|
||||
if($contents->isEmpty()) return;
|
||||
|
||||
Cache::put($cache_name, base64_encode( $contents->toJson()), 600);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @OA\Post(
|
||||
* path="/api/v1/import_json",
|
||||
* operationId="getImportJson",
|
||||
* tags={"import"},
|
||||
* summary="Import data from the system",
|
||||
* description="Import data from the system",
|
||||
* @OA\Parameter(ref="#/components/parameters/X-Requested-With"),
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="success",
|
||||
* @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"),
|
||||
* @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"),
|
||||
* @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"),
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=422,
|
||||
* description="Validation error",
|
||||
* @OA\JsonContent(ref="#/components/schemas/ValidationError"),
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response="default",
|
||||
* description="Unexpected Error",
|
||||
* @OA\JsonContent(ref="#/components/schemas/Error"),
|
||||
* ),
|
||||
* )
|
||||
*/
|
||||
public function import(Request $request)
|
||||
{
|
||||
$hash = Str::random(32);
|
||||
foreach($request->input('import_types') as $type)
|
||||
{
|
||||
$this->preimport($type, $hash);
|
||||
}
|
||||
/** @var \App\Models\User $user */
|
||||
$user = auth()->user() ?? Auth::loginUsingId(60);
|
||||
$data = ['import_types' => $request->input('import_types') ] + compact('hash');
|
||||
if (Ninja::isHosted()) {
|
||||
QuickbooksIngest::dispatch( $data , $user->company() );
|
||||
} else {
|
||||
QuickbooksIngest::dispatch($data, $user->company() );
|
||||
}
|
||||
|
||||
return response()->json(['message' => 'Processing'], 200);
|
||||
}
|
||||
}
|
252
app/Import/Providers/Quickbooks.php
Normal file
252
app/Import/Providers/Quickbooks.php
Normal file
@ -0,0 +1,252 @@
|
||||
<?php
|
||||
/**
|
||||
* Invoice Ninja (https://invoiceninja.com).
|
||||
*
|
||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||
*
|
||||
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
*
|
||||
* @license https://www.elastic.co/licensing/elastic-license
|
||||
*/
|
||||
|
||||
namespace App\Import\Providers;
|
||||
|
||||
use App\Models\Invoice;
|
||||
use App\Factory\ProductFactory;
|
||||
use App\Factory\ClientFactory;
|
||||
use App\Factory\InvoiceFactory;
|
||||
use App\Factory\PaymentFactory;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use App\Repositories\ClientRepository;
|
||||
use App\Repositories\InvoiceRepository;
|
||||
use App\Repositories\ProductRepository;
|
||||
use App\Repositories\PaymentRepository;
|
||||
use App\Http\Requests\Client\StoreClientRequest;
|
||||
use App\Http\Requests\Product\StoreProductRequest;
|
||||
use App\Http\Requests\Invoice\StoreInvoiceRequest;
|
||||
use App\Http\Requests\Payment\StorePaymentRequest;
|
||||
use App\Import\Transformer\Quickbooks\ClientTransformer;
|
||||
use App\Import\Transformer\Quickbooks\InvoiceTransformer;
|
||||
use App\Import\Transformer\Quickbooks\ProductTransformer;
|
||||
use App\Import\Transformer\Quickbooks\PaymentTransformer;
|
||||
|
||||
class Quickbooks extends BaseImport
|
||||
{
|
||||
public array $entity_count = [];
|
||||
|
||||
public function import(string $entity)
|
||||
{
|
||||
if (
|
||||
in_array($entity, [
|
||||
'client',
|
||||
'invoice',
|
||||
'product',
|
||||
'payment',
|
||||
// 'vendor',
|
||||
// 'expense',
|
||||
])
|
||||
) {
|
||||
$this->{$entity}();
|
||||
}
|
||||
|
||||
//collate any errors
|
||||
|
||||
// $this->finalizeImport();
|
||||
}
|
||||
|
||||
public function client()
|
||||
{
|
||||
$entity_type = 'client';
|
||||
$data = $this->getData($entity_type);
|
||||
if (empty($data)) {
|
||||
$this->entity_count['clients'] = 0;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->request_name = StoreClientRequest::class;
|
||||
$this->repository_name = ClientRepository::class;
|
||||
$this->factory_name = ClientFactory::class;
|
||||
$this->repository = app()->make($this->repository_name);
|
||||
$this->repository->import_mode = true;
|
||||
$this->transformer = new ClientTransformer($this->company);
|
||||
$client_count = $this->ingest($data, $entity_type);
|
||||
$this->entity_count['clients'] = $client_count;
|
||||
}
|
||||
|
||||
public function product()
|
||||
{
|
||||
$entity_type = 'product';
|
||||
$data = $this->getData($entity_type);
|
||||
if (empty($data)) {
|
||||
$this->entity_count['products'] = 0;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->request_name = StoreProductRequest::class;
|
||||
$this->repository_name = ProductRepository::class;
|
||||
$this->factory_name = ProductFactory::class;
|
||||
$this->repository = app()->make($this->repository_name);
|
||||
$this->repository->import_mode = true;
|
||||
$this->transformer = new ProductTransformer($this->company);
|
||||
$count = $this->ingest($data, $entity_type);
|
||||
$this->entity_count['products'] = $count;
|
||||
}
|
||||
|
||||
public function getData($type) {
|
||||
|
||||
// get the data from cache? file? or api ?
|
||||
return json_decode(base64_decode(Cache::get("{$this->hash}-$type")), 1);
|
||||
}
|
||||
|
||||
public function payment()
|
||||
{
|
||||
$entity_type = 'payment';
|
||||
$data = $this->getData($entity_type);
|
||||
if (empty($data)) {
|
||||
$this->entity_count['payments'] = 0;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->request_name = StorePaymentRequest::class;
|
||||
$this->repository_name = PaymentRepository::class;
|
||||
$this->factory_name = PaymentFactory::class;
|
||||
$this->repository = app()->make($this->repository_name);
|
||||
$this->repository->import_mode = true;
|
||||
$this->transformer = new PaymentTransformer($this->company);
|
||||
$count = $this->ingest($data, $entity_type);
|
||||
$this->entity_count['payments'] = $count;
|
||||
}
|
||||
|
||||
public function invoice()
|
||||
{
|
||||
//make sure we update and create products
|
||||
$initial_update_products_value = $this->company->update_products;
|
||||
$this->company->update_products = true;
|
||||
|
||||
$this->company->save();
|
||||
|
||||
$entity_type = 'invoice';
|
||||
$data = $this->getData($entity_type);
|
||||
|
||||
if (empty($data)) {
|
||||
$this->entity_count['invoices'] = 0;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->request_name = StoreInvoiceRequest::class;
|
||||
$this->repository_name = InvoiceRepository::class;
|
||||
$this->factory_name = InvoiceFactory::class;
|
||||
$this->repository = app()->make($this->repository_name);
|
||||
$this->repository->import_mode = true;
|
||||
$this->transformer = new InvoiceTransformer($this->company);
|
||||
$invoice_count = $this->ingestInvoices($data,'');
|
||||
$this->entity_count['invoices'] = $invoice_count;
|
||||
$this->company->update_products = $initial_update_products_value;
|
||||
$this->company->save();
|
||||
}
|
||||
|
||||
public function ingestInvoices($invoices, $invoice_number_key)
|
||||
{
|
||||
$count = 0;
|
||||
$invoice_transformer = $this->transformer;
|
||||
/** @var ClientRepository $client_repository */
|
||||
$client_repository = app()->make(ClientRepository::class);
|
||||
$client_repository->import_mode = true;
|
||||
$invoice_repository = new InvoiceRepository();
|
||||
$invoice_repository->import_mode = true;
|
||||
|
||||
foreach ($invoices as $raw_invoice) {
|
||||
if(!is_array($raw_invoice)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
$invoice_data = $invoice_transformer->transform($raw_invoice);
|
||||
$invoice_data['user_id'] = $this->company->owner()->id;
|
||||
$invoice_data['line_items'] = (array) $invoice_data['line_items'];
|
||||
$invoice_data['line_items'] = $this->cleanItems(
|
||||
$invoice_data['line_items'] ?? []
|
||||
);
|
||||
|
||||
if (
|
||||
empty($invoice_data['client_id']) &&
|
||||
! empty($invoice_data['client'])
|
||||
) {
|
||||
$client_data = $invoice_data['client'];
|
||||
$client_data['user_id'] = $this->getUserIDForRecord(
|
||||
$invoice_data
|
||||
);
|
||||
$client_repository->save(
|
||||
$client_data,
|
||||
$client = ClientFactory::create(
|
||||
$this->company->id,
|
||||
$client_data['user_id']
|
||||
)
|
||||
);
|
||||
$invoice_data['client_id'] = $client->id;
|
||||
unset($invoice_data['client']);
|
||||
}
|
||||
|
||||
$validator = $this->request_name::runFormRequest($invoice_data);
|
||||
if ($validator->fails()) {
|
||||
$this->error_array['invoice'][] = [
|
||||
'invoice' => $invoice_data,
|
||||
'error' => $validator->errors()->all(),
|
||||
];
|
||||
} else {
|
||||
if(!Invoice::where('number',$invoice_data['number'])->get()->first())
|
||||
{
|
||||
$invoice = InvoiceFactory::create(
|
||||
$this->company->id,
|
||||
$this->company->owner()->id
|
||||
);
|
||||
$invoice->mergeFillable(['partial','amount','balance','line_items']);
|
||||
if (! empty($invoice_data['status_id'])) {
|
||||
$invoice->status_id = $invoice_data['status_id'];
|
||||
}
|
||||
|
||||
$saveable_invoice_data = $invoice_data;
|
||||
if(array_key_exists('payments', $saveable_invoice_data)) {
|
||||
unset($saveable_invoice_data['payments']);
|
||||
}
|
||||
|
||||
$invoice->fill($saveable_invoice_data);
|
||||
$invoice->save();
|
||||
$count++;
|
||||
|
||||
}
|
||||
// $this->actionInvoiceStatus(
|
||||
// $invoice,
|
||||
// $invoice_data,
|
||||
// $invoice_repository
|
||||
// );
|
||||
}
|
||||
} catch (\Exception $ex) {
|
||||
if (\DB::connection(config('database.default'))->transactionLevel() > 0) {
|
||||
\DB::connection(config('database.default'))->rollBack();
|
||||
}
|
||||
|
||||
if ($ex instanceof ImportException) {
|
||||
$message = $ex->getMessage();
|
||||
} else {
|
||||
report($ex);
|
||||
$message = 'Unknown error ';
|
||||
nlog($ex->getMessage());
|
||||
nlog($raw_invoice);
|
||||
}
|
||||
|
||||
$this->error_array['invoice'][] = [
|
||||
'invoice' => $raw_invoice,
|
||||
'error' => $message,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
}
|
95
app/Import/Transformer/Quickbooks/ClientTransformer.php
Normal file
95
app/Import/Transformer/Quickbooks/ClientTransformer.php
Normal file
@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Invoice Ninja (https://clientninja.com).
|
||||
*
|
||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||
*
|
||||
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
*
|
||||
* @license https://www.elastic.co/licensing/elastic-license
|
||||
*/
|
||||
|
||||
namespace App\Import\Transformer\Quickbooks;
|
||||
|
||||
use App\Import\Transformer\Quickbooks\CommonTrait;
|
||||
use App\Import\Transformer\BaseTransformer;
|
||||
use App\Models\Client as Model;
|
||||
use App\Models\ClientContact;
|
||||
use App\Import\ImportException;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* Class ClientTransformer.
|
||||
*/
|
||||
class ClientTransformer extends BaseTransformer
|
||||
{
|
||||
|
||||
use CommonTrait {
|
||||
transform as preTransform;
|
||||
}
|
||||
|
||||
private $fillable = [
|
||||
'name' => 'CompanyName',
|
||||
'phone' => 'PrimaryPhone.FreeFormNumber',
|
||||
'country_id' => 'BillAddr.Country',
|
||||
'state' => 'BillAddr.CountrySubDivisionCode',
|
||||
'address1' => 'BillAddr.Line1',
|
||||
'city' => 'BillAddr.City',
|
||||
'postal_code' => 'BillAddr.PostalCode',
|
||||
'shipping_country_id' => 'ShipAddr.Country',
|
||||
'shipping_state' => 'ShipAddr.CountrySubDivisionCode',
|
||||
'shipping_address1' => 'ShipAddr.Line1',
|
||||
'shipping_city' => 'ShipAddr.City',
|
||||
'shipping_postal_code' => 'ShipAddr.PostalCode',
|
||||
'public_notes' => 'Notes'
|
||||
];
|
||||
|
||||
public function __construct($company)
|
||||
{
|
||||
parent::__construct($company);
|
||||
|
||||
$this->model = new Model;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Transforms a Customer array into a ClientContact model.
|
||||
*
|
||||
* @param array $data
|
||||
* @return array|bool
|
||||
*/
|
||||
public function transform($data)
|
||||
{
|
||||
$transformed_data = [];
|
||||
// Assuming 'customer_name' is equivalent to 'CompanyName'
|
||||
if (isset($data['CompanyName']) && $this->hasClient($data['CompanyName'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$transformed_data = $this->preTransform($data);
|
||||
$transformed_data['contacts'][0] = $this->getContacts($data)->toArray()+['company_id' => $this->company->id ];
|
||||
|
||||
return $transformed_data;
|
||||
}
|
||||
|
||||
protected function getContacts($data) {
|
||||
return (new ClientContact())->fill([
|
||||
'first_name' => $this->getString($data, 'GivenName'),
|
||||
'last_name' => $this->getString($data, 'FamilyName'),
|
||||
'phone' => $this->getString($data, 'PrimaryPhone.FreeFormNumber'),
|
||||
'email' => $this->getString($data, 'PrimaryEmailAddr.Address'),
|
||||
'company_id' => $this->company->id
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
public function getShipAddrCountry($data,$field) {
|
||||
return is_null(($c = $this->getString($data,$field))) ? null : $this->getCountryId($c);
|
||||
}
|
||||
|
||||
public function getBillAddrCountry($data,$field) {
|
||||
return is_null(($c = $this->getString($data,$field))) ? null : $this->getCountryId($c);
|
||||
}
|
||||
|
||||
}
|
36
app/Import/Transformer/Quickbooks/CommonTrait.php
Normal file
36
app/Import/Transformer/Quickbooks/CommonTrait.php
Normal file
@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\Import\Transformer\Quickbooks;
|
||||
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
trait CommonTrait
|
||||
{
|
||||
protected $model;
|
||||
|
||||
public function getString($data,$field) {
|
||||
return Arr::get($data,$field);
|
||||
}
|
||||
|
||||
public function getCreateTime($data, $field = null)
|
||||
{
|
||||
return $this->parseDateOrNull($data, 'MetaData.CreateTime');
|
||||
}
|
||||
|
||||
public function getLastUpdatedTime($data, $field = null)
|
||||
{
|
||||
return $this->parseDateOrNull($data,'MetaData.LastUpdatedTime');
|
||||
}
|
||||
|
||||
public function transform($data)
|
||||
{
|
||||
$transformed = [];
|
||||
|
||||
foreach ($this->fillable as $key => $field) {
|
||||
$transformed[$key] = is_null((($v = $this->getString($data, $field))))? null : (method_exists($this, ($method = "get{$field}")) ? call_user_func([$this, $method], $data, $field ) : $this->getString($data,$field));
|
||||
}
|
||||
|
||||
return $this->model->fillable(array_keys($this->fillable))->fill($transformed)->toArray() + ['company_id' => $this->company->id ] ;
|
||||
}
|
||||
|
||||
}
|
196
app/Import/Transformer/Quickbooks/InvoiceTransformer.php
Normal file
196
app/Import/Transformer/Quickbooks/InvoiceTransformer.php
Normal file
@ -0,0 +1,196 @@
|
||||
<?php
|
||||
/**
|
||||
* Invoice Ninja (https://invoiceninja.com).
|
||||
*
|
||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||
*
|
||||
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
*
|
||||
* @license https://www.elastic.co/licensing/elastic-license
|
||||
*/
|
||||
|
||||
namespace App\Import\Transformer\Quickbooks;
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Support\Arr;
|
||||
use App\Import\ImportException;
|
||||
use App\DataMapper\InvoiceItem;
|
||||
use App\Models\Invoice as Model;
|
||||
use App\Import\Transformer\BaseTransformer;
|
||||
use App\Import\Transformer\Quickbooks\CommonTrait;
|
||||
use App\Import\Transformer\Quickbooks\ClientTransformer;
|
||||
|
||||
/**
|
||||
* Class InvoiceTransformer.
|
||||
*/
|
||||
class InvoiceTransformer extends BaseTransformer
|
||||
{
|
||||
use CommonTrait {
|
||||
transform as preTransform;
|
||||
}
|
||||
|
||||
private $fillable = [
|
||||
'amount' => "TotalAmt",
|
||||
'line_items' => "Line",
|
||||
'due_date' => "DueDate",
|
||||
'partial' => "Deposit",
|
||||
'balance' => "Balance",
|
||||
'private_notes' => "CustomerMemo",
|
||||
'public_notes' => "CustomerMemo",
|
||||
'number' => "DocNumber",
|
||||
'created_at' => "CreateTime",
|
||||
'updated_at' => "LastUpdatedTime",
|
||||
'payments' => 'LinkedTxn',
|
||||
'status_id' => 'InvoiceStatus',
|
||||
];
|
||||
|
||||
public function __construct($company)
|
||||
{
|
||||
parent::__construct($company);
|
||||
|
||||
$this->model = new Model;
|
||||
}
|
||||
|
||||
public function getInvoiceStatus($data)
|
||||
{
|
||||
return Invoice::STATUS_SENT;
|
||||
}
|
||||
|
||||
public function transform($data)
|
||||
{
|
||||
return $this->preTransform($data) + $this->getInvoiceClient($data);
|
||||
}
|
||||
|
||||
public function getTotalAmt($data)
|
||||
{
|
||||
return (float) $this->getString($data,'TotalAmt');
|
||||
}
|
||||
|
||||
public function getLine($data)
|
||||
{
|
||||
return array_map(function ($item) {
|
||||
return [
|
||||
'description' => $this->getString($item,'Description'),
|
||||
'product_key' => $this->getString($item,'Description'),
|
||||
'quantity' => (int) $this->getString($item,'SalesItemLineDetail.Qty'),
|
||||
'unit_price' =>(double) $this->getString($item,'SalesItemLineDetail.UnitPrice'),
|
||||
'line_total' => (double) $this->getString($item,'Amount'),
|
||||
'cost' =>(double) $this->getString($item,'SalesItemLineDetail.UnitPrice'),
|
||||
'product_cost' => (double) $this->getString($item,'SalesItemLineDetail.UnitPrice'),
|
||||
'tax_amount' => (double) $this->getString($item,'TxnTaxDetail.TotalTax'),
|
||||
];
|
||||
}, array_filter($this->getString($data,'Line'), function ($item) {
|
||||
return $this->getString($item,'DetailType') !== 'SubTotalLineDetail';
|
||||
}));
|
||||
}
|
||||
|
||||
public function getInvoiceClient($data, $field = null) {
|
||||
/**
|
||||
* "CustomerRef": {
|
||||
"value": "23",
|
||||
"name": ""Barnett Design
|
||||
},
|
||||
"CustomerMemo": {
|
||||
"value": "Thank you for your business and have a great day!"
|
||||
},
|
||||
"BillAddr": {
|
||||
"Id": "58",
|
||||
"Line1": "Shara Barnett",
|
||||
"Line2": "Barnett Design",
|
||||
"Line3": "19 Main St.",
|
||||
"Line4": "Middlefield, CA 94303",
|
||||
"Lat": "37.4530553",
|
||||
"Long": "-122.1178261"
|
||||
},
|
||||
"ShipAddr": {
|
||||
"Id": "24",
|
||||
"Line1": "19 Main St.",
|
||||
"City": "Middlefield",
|
||||
"CountrySubDivisionCode": "CA",
|
||||
"PostalCode": "94303",
|
||||
"Lat": "37.445013",
|
||||
"Long": "-122.1391443"
|
||||
},"BillEmail": {
|
||||
"Address": "Design@intuit.com"
|
||||
},
|
||||
[
|
||||
'name' => 'CompanyName',
|
||||
'phone' => 'PrimaryPhone.FreeFormNumber',
|
||||
'country_id' => 'BillAddr.Country',
|
||||
'state' => 'BillAddr.CountrySubDivisionCode',
|
||||
'address1' => 'BillAddr.Line1',
|
||||
'city' => 'BillAddr.City',
|
||||
'postal_code' => 'BillAddr.PostalCode',
|
||||
'shipping_country_id' => 'ShipAddr.Country',
|
||||
'shipping_state' => 'ShipAddr.CountrySubDivisionCode',
|
||||
'shipping_address1' => 'ShipAddr.Line1',
|
||||
'shipping_city' => 'ShipAddr.City',
|
||||
'shipping_postal_code' => 'ShipAddr.PostalCode',
|
||||
'public_notes' => 'Notes'
|
||||
];
|
||||
|
||||
*/
|
||||
$bill_address = (object) $this->getString($data, 'BillAddr');
|
||||
$ship_address = $this->getString($data, 'ShipAddr');
|
||||
$customer = explode(" ", $this->getString($data, 'CustomerRef.name'));
|
||||
$customer = ['GivenName' => $customer[0], 'FamilyName' => $customer[1]];
|
||||
$has_company = property_exists($bill_address, 'Line4');
|
||||
$address = $has_company? $bill_address->Line4 : $bill_address->Line3;
|
||||
$address_1 = substr($address, 0, stripos($address,','));
|
||||
$address =array_filter( [$address_1] + (explode(' ', substr($address, stripos($address,",") + 1 ))));
|
||||
$client_id = null;
|
||||
$client =
|
||||
[
|
||||
"CompanyName" => $has_company? $bill_address->Line2 : $bill_address->Line1,
|
||||
"BillAddr" => array_combine(['City','CountrySubDivisionCode','PostalCode'], array_pad($address,3,'N/A') ) + ['Line1' => $has_company? $bill_address->Line3 : $bill_address->Line2 ],
|
||||
"ShipAddr" => $ship_address
|
||||
] + $customer + ['PrimaryEmailAddr' => ['Address' => $this->getString($data, 'BillEmail.Address') ]];
|
||||
if($this->hasClient($client['CompanyName']))
|
||||
{
|
||||
$client_id = $this->getClient($client['CompanyName'],$this->getString($client, 'PrimaryEmailAddr.Address'));
|
||||
}
|
||||
|
||||
|
||||
return ['client'=> (new ClientTransformer($this->company))->transform($client), 'client_id'=> $client_id ];
|
||||
}
|
||||
|
||||
public function getDueDate($data)
|
||||
{
|
||||
return $this->parseDateOrNull($data, 'DueDate');
|
||||
}
|
||||
|
||||
public function getDeposit($data)
|
||||
{
|
||||
return (double) $this->getString($data,'Deposit');
|
||||
}
|
||||
|
||||
public function getBalance($data)
|
||||
{
|
||||
return (double) $this->getString($data,'Balance');
|
||||
}
|
||||
|
||||
public function getCustomerMemo($data)
|
||||
{
|
||||
return $this->getString($data,'CustomerMemo.value');
|
||||
}
|
||||
|
||||
public function getDocNumber($data, $field = null)
|
||||
{
|
||||
return sprintf("%s-%s",
|
||||
$this->getString($data, 'DocNumber'),
|
||||
$this->getString($data, 'Id.value')
|
||||
);
|
||||
}
|
||||
|
||||
public function getLinkedTxn($data)
|
||||
{
|
||||
$payments = $this->getString($data,'LinkedTxn');
|
||||
if(empty($payments)) return [];
|
||||
|
||||
return [[
|
||||
'amount' => $this->getTotalAmt($data),
|
||||
'date' => $this->parseDateOrNull($data, 'TxnDate')
|
||||
]];
|
||||
|
||||
}
|
||||
}
|
99
app/Import/Transformer/Quickbooks/PaymentTransformer.php
Normal file
99
app/Import/Transformer/Quickbooks/PaymentTransformer.php
Normal file
@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Invoice Ninja (https://Paymentninja.com).
|
||||
*
|
||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||
*
|
||||
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
*
|
||||
* @license https://www.elastic.co/licensing/elastic-license
|
||||
*/
|
||||
|
||||
namespace App\Import\Transformer\Quickbooks;
|
||||
|
||||
use App\Import\Transformer\Quickbooks\CommonTrait;
|
||||
use App\Import\Transformer\BaseTransformer;
|
||||
use App\Models\Payment as Model;
|
||||
use App\Import\ImportException;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Support\Arr;
|
||||
use App\Models\Invoice;
|
||||
|
||||
/**
|
||||
*
|
||||
* Class PaymentTransformer.
|
||||
*/
|
||||
class PaymentTransformer extends BaseTransformer
|
||||
{
|
||||
use CommonTrait;
|
||||
|
||||
protected $fillable = [
|
||||
'number' => "PaymentRefNum",
|
||||
'amount' => "TotalAmt",
|
||||
"client_id" => "CustomerRef",
|
||||
"currency_id" => "CurrencyRef",
|
||||
'date' => "TxnDate",
|
||||
"invoices" => "Line",
|
||||
'private_notes' => "PrivateNote",
|
||||
'created_at' => "CreateTime",
|
||||
'updated_at' => "LastUpdatedTime"
|
||||
];
|
||||
|
||||
public function __construct($company)
|
||||
{
|
||||
parent::__construct($company);
|
||||
|
||||
$this->model = new Model;
|
||||
}
|
||||
|
||||
public function getTotalAmt($data, $field = null) {
|
||||
return (float) $this->getString($data, $field);
|
||||
}
|
||||
|
||||
public function getTxnDate($data, $field = null)
|
||||
{
|
||||
return $this->parseDateOrNull($data, $field);
|
||||
}
|
||||
|
||||
public function getCustomerRef($data, $field = null )
|
||||
{
|
||||
return $this->getClient($this->getString($data, 'CustomerRef.name'),null);
|
||||
}
|
||||
|
||||
public function getCurrencyRef($data, $field = null)
|
||||
{
|
||||
return $this->getCurrencyByCode($data['CurrencyRef'], 'value');
|
||||
}
|
||||
|
||||
public function getLine($data, $field = null)
|
||||
{
|
||||
$invoices = [];
|
||||
$invoice = $this->getString($data,'Line.LinkedTxn.TxnType');
|
||||
if(is_null($invoice) || $invoice !== 'Invoice') return $invoices;
|
||||
if( is_null( ($invoice_id = $this->getInvoiceId($this->getString($data, 'Line.LinkedTxn.TxnId.value')))) ) return $invoices;
|
||||
|
||||
return [[
|
||||
'amount' => (float) $this->getString($data, 'Line.Amount'),
|
||||
'invoice_id' => $invoice_id
|
||||
]];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $invoice_number
|
||||
*
|
||||
* @return int|null
|
||||
*/
|
||||
public function getInvoiceId($invoice_number)
|
||||
{
|
||||
$invoice = Invoice::query()->where('company_id', $this->company->id)
|
||||
->where('is_deleted', false)
|
||||
->where("number", "LIKE",
|
||||
"%-$invoice_number%",
|
||||
)
|
||||
->first();
|
||||
|
||||
return $invoice ? $invoice->id : null;
|
||||
}
|
||||
|
||||
}
|
59
app/Import/Transformer/Quickbooks/ProductTransformer.php
Normal file
59
app/Import/Transformer/Quickbooks/ProductTransformer.php
Normal file
@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Invoice Ninja (https://Productninja.com).
|
||||
*
|
||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||
*
|
||||
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
*
|
||||
* @license https://www.elastic.co/licensing/elastic-license
|
||||
*/
|
||||
|
||||
namespace App\Import\Transformer\Quickbooks;
|
||||
|
||||
use App\Import\Transformer\Quickbooks\CommonTrait;
|
||||
use App\Import\Transformer\BaseTransformer;
|
||||
use App\Models\Product as Model;
|
||||
use App\Import\ImportException;
|
||||
|
||||
/**
|
||||
* Class ProductTransformer.
|
||||
*/
|
||||
class ProductTransformer extends BaseTransformer
|
||||
{
|
||||
|
||||
use CommonTrait;
|
||||
|
||||
protected $fillable = [
|
||||
'product_key' => 'Name',
|
||||
'notes' => 'Description',
|
||||
'cost' => 'PurchaseCost',
|
||||
'price' => 'UnitPrice',
|
||||
'quantity' => 'QtyOnHand',
|
||||
'in_stock_quantity' => 'QtyOnHand',
|
||||
'created_at' => 'CreateTime',
|
||||
'updated_at' => 'LastUpdatedTime',
|
||||
];
|
||||
|
||||
|
||||
public function __construct($company)
|
||||
{
|
||||
parent::__construct($company);
|
||||
|
||||
$this->model = new Model;
|
||||
}
|
||||
|
||||
public function getQtyOnHand($data, $field = null) {
|
||||
return (int) $this->getString($data, $field);
|
||||
}
|
||||
|
||||
public function getPurchaseCost($data, $field = null) {
|
||||
return (double) $this->getString($data, $field);
|
||||
}
|
||||
|
||||
|
||||
public function getUnitPrice($data, $field = null) {
|
||||
return (float) $this->getString($data, $field);
|
||||
}
|
||||
}
|
45
app/Jobs/Import/QuickbooksIngest.php
Normal file
45
app/Jobs/Import/QuickbooksIngest.php
Normal file
@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs\Import;
|
||||
|
||||
use App\Libraries\MultiDB;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use App\Import\Providers\Quickbooks;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
|
||||
class QuickbooksIngest implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $engine;
|
||||
protected $request;
|
||||
protected $company;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*/
|
||||
public function __construct(array $request, $company)
|
||||
{
|
||||
$this->company = $company;
|
||||
$this->request = $request;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*/
|
||||
public function handle(): void
|
||||
{
|
||||
MultiDB::setDb($this->company->db);
|
||||
set_time_limit(0);
|
||||
|
||||
$engine = new Quickbooks(['import_type' => 'client', 'hash'=> $this->request['hash'] ], $this->company);
|
||||
foreach ($this->request['import_types'] as $entity) {
|
||||
$engine->import($entity);
|
||||
}
|
||||
|
||||
$engine->finalizeImport();
|
||||
}
|
||||
}
|
84
app/Providers/QuickbooksServiceProvider.php
Normal file
84
app/Providers/QuickbooksServiceProvider.php
Normal file
@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use App\Factory\QuickbooksSDKFactory;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use App\Http\Controllers\ImportQuickbooksController;
|
||||
use App\Services\Import\Quickbooks\Service as QuickbooksService;
|
||||
use App\Repositories\Import\Quickcbooks\Contracts\RepositoryInterface;
|
||||
use App\Services\Import\Quickbooks\SdkWrapper as QuickbooksSDKWrapper;
|
||||
use App\Services\Import\Quickbooks\Contracts\SdkInterface as QuickbooksInterface;
|
||||
use App\Services\Import\Quickbooks\Transformers\Transformer as QuickbooksTransformer;
|
||||
|
||||
class QuickbooksServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Register services.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register()
|
||||
{
|
||||
|
||||
$this->app->bind(QuickbooksInterface::class, function ($app) {
|
||||
return new QuickbooksSDKWrapper(QuickbooksSDKFactory::create());
|
||||
});
|
||||
|
||||
// Register SDKWrapper with DataService dependency
|
||||
$this->app->singleton(QuickbooksService::class, function ($app) {
|
||||
return new QuickbooksService($app->make(QuickbooksInterface::class));
|
||||
});
|
||||
|
||||
$this->app->singleton(QuickbooksTransformer::class,QuickbooksTransformer::class);
|
||||
}
|
||||
/**
|
||||
* Bootstrap services.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function boot()
|
||||
{
|
||||
$this->registerRoutes();
|
||||
$this->registerConfig();
|
||||
}
|
||||
|
||||
protected function registerConfig() {
|
||||
config()->set( 'services.quickbooks' ,
|
||||
['settings' => [
|
||||
'auth_mode' => 'oauth2',
|
||||
'ClientID' => env('QUICKBOOKS_CLIENT_ID', false),
|
||||
'ClientSecret' => env('QUICKBOOKS_CLIENT_SECRET', false),
|
||||
// TODO use env('QUICKBOOKS_REDIRECT_URI') or route()/ url()
|
||||
'RedirectURI' => url("/quickbooks/authorized"),
|
||||
'scope' => "com.intuit.quickbooks.accounting",
|
||||
'baseUrl' => ucfirst(env('APP_ENV'))
|
||||
],
|
||||
'debug' => env('APP_DEBUG') || env('APP_ENV')
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register custom routes.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function registerRoutes()
|
||||
{
|
||||
Route::middleware('web')
|
||||
->namespace($this->app->getNamespace() . 'Http\Controllers')
|
||||
->group(function () {
|
||||
Route::get('quickbooks/authorize/{token}', [ImportQuickbooksController::class, 'authorizeQuickbooks'])->name('authorize.quickbooks');
|
||||
Route::get('quickbooks/authorized', [ImportQuickbooksController::class, 'onAuthorized'])->name('authorized.quickbooks');
|
||||
});
|
||||
Route::prefix('api/v1')
|
||||
->middleware('api')
|
||||
->namespace($this->app->getNamespace() . 'Http\Controllers')
|
||||
->group(function () {
|
||||
Route::post('import/quickbooks', [ImportQuickbooksController::class, 'import'])->name('import.quickbooks');
|
||||
});
|
||||
}
|
||||
}
|
@ -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";
|
||||
}
|
10
app/Repositories/Import/Quickbooks/InvoiceRepository.php
Normal file
10
app/Repositories/Import/Quickbooks/InvoiceRepository.php
Normal file
@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Repositories\Import\Quickbooks;
|
||||
|
||||
use App\Repositories\Import\Quickbooks\Contracts\RepositoryInterface as QuickbooksInterface;
|
||||
|
||||
class InvoiceRepository extends Repository implements QuickbooksInterface
|
||||
{
|
||||
protected string $entity = "Invoice";
|
||||
}
|
11
app/Repositories/Import/Quickbooks/ItemRepository.php
Normal file
11
app/Repositories/Import/Quickbooks/ItemRepository.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 ItemRepository extends Repository implements RepositoryInterface
|
||||
{
|
||||
protected string $entity = "Item";
|
||||
}
|
10
app/Repositories/Import/Quickbooks/PaymentRepository.php
Normal file
10
app/Repositories/Import/Quickbooks/PaymentRepository.php
Normal file
@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Repositories\Import\Quickbooks;
|
||||
|
||||
use App\Repositories\Import\Quickbooks\Contracts\RepositoryInterface as QuickbooksInterface;
|
||||
|
||||
class PaymentRepository extends Repository implements QuickbooksInterface
|
||||
{
|
||||
protected string $entity = "Payment";
|
||||
}
|
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,86 @@
|
||||
<?php
|
||||
|
||||
namespace App\Repositories\Import\Quickbooks\Transformers;
|
||||
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class Transformer
|
||||
{
|
||||
public function transform(array $items, string $type): Collection
|
||||
{
|
||||
if(!method_exists($this, ($method = "transform{$type}s"))) throw new \InvalidArgumentException("Unknown type: $type");
|
||||
|
||||
return call_user_func([$this, $method], $items);
|
||||
}
|
||||
|
||||
protected function transformCustomers(array $items): Collection
|
||||
{
|
||||
return $this->transformation($items, [
|
||||
'CompanyName',
|
||||
'PrimaryPhone',
|
||||
'BillAddr',
|
||||
'ShipAddr',
|
||||
'Notes',
|
||||
'GivenName',
|
||||
'FamilyName',
|
||||
'PrimaryEmailAddr',
|
||||
'CurrencyRef',
|
||||
'MetaData'
|
||||
]);
|
||||
}
|
||||
|
||||
protected function transformInvoices(array $items): Collection
|
||||
{
|
||||
return $this->transformation($items, [
|
||||
"TotalAmt",
|
||||
"Line",
|
||||
"DueDate",
|
||||
"Deposit",
|
||||
"Balance",
|
||||
"CustomerMemo",
|
||||
"DocNumber",
|
||||
"CustomerRef",
|
||||
"BillEmail",
|
||||
'MetaData',
|
||||
"BillAddr",
|
||||
"ShipAddr",
|
||||
"LinkedTxn",
|
||||
"Id",
|
||||
"CurrencyRef",
|
||||
"TxnTaxDetail",
|
||||
"TxnDate"
|
||||
]);
|
||||
}
|
||||
|
||||
protected function transformPayments(array $items): Collection
|
||||
{
|
||||
return $this->transformation($items, [
|
||||
"PaymentRefNum",
|
||||
"TotalAmt",
|
||||
"CustomerRef",
|
||||
"CurrencyRef",
|
||||
"TxnDate",
|
||||
"Line",
|
||||
"PrivateNote",
|
||||
"MetaData"
|
||||
]);
|
||||
}
|
||||
|
||||
protected function transformItems(array $items): Collection
|
||||
{
|
||||
return $this->transformation($items, [
|
||||
'Name',
|
||||
'Description',
|
||||
'PurchaseCost',
|
||||
'UnitPrice',
|
||||
'QtyOnHand',
|
||||
'MetaData'
|
||||
]);
|
||||
}
|
||||
|
||||
protected function transformation(array $items, array $keys) : Collection
|
||||
{
|
||||
return collect($items)->select($keys);
|
||||
}
|
||||
|
||||
}
|
66
app/Services/Import/Quickbooks/Auth.php
Normal file
66
app/Services/Import/Quickbooks/Auth.php
Normal file
@ -0,0 +1,66 @@
|
||||
<?php
|
||||
namespace App\Services\Import\Quickbooks;
|
||||
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use App\Services\Import\Quickbooks\Repositories\CompanyTokensRepository;
|
||||
use App\Services\Import\QuickBooks\Contracts\SDKInterface as QuickbooksInterface;
|
||||
|
||||
final class Auth
|
||||
{
|
||||
private QuickbooksInterface $sdk;
|
||||
|
||||
public function __construct(QuickbooksInterface $quickbooks) {
|
||||
$this->sdk = $quickbooks;
|
||||
}
|
||||
|
||||
public function accessToken(string $code, string $realm ) : array
|
||||
{
|
||||
// TODO: Get or put token in Cache or DB?
|
||||
return $this->sdk->accessToken($code, $realm);
|
||||
}
|
||||
|
||||
public function refreshToken() : array
|
||||
{
|
||||
// TODO: Get or put token in Cache or DB?
|
||||
return $this->sdk->refreshToken();
|
||||
}
|
||||
|
||||
public function getAuthorizationUrl(): string
|
||||
{
|
||||
return $this->sdk->getAuthorizationUrl();
|
||||
}
|
||||
|
||||
public function getState() : string
|
||||
{
|
||||
return $this->sdk->getState();
|
||||
}
|
||||
|
||||
public function saveTokens($key, $tokens)
|
||||
{
|
||||
$token_store = new CompanyTokensRepository($key);
|
||||
$token_store->save($tokens);
|
||||
}
|
||||
|
||||
public function getAccessToken() : array
|
||||
{
|
||||
$token_store = new CompanyTokensRepository();
|
||||
$tokens = $token_store->get();
|
||||
if(empty($tokens)) {
|
||||
$token = $this->sdk->getAccessToken();
|
||||
$access_token = $token->getAccessToken();
|
||||
$realm = $token->getRealmID();
|
||||
$refresh_token = $token->getRefreshToken();
|
||||
$access_token_expires = $token->getAccessTokenExpiresAt();
|
||||
$refresh_token_expires = $token->getRefreshTokenExpiresAt();
|
||||
$tokens = compact('access_token', 'refresh_token','access_token_expires', 'refresh_token_expires','realm');
|
||||
}
|
||||
|
||||
return $tokens;
|
||||
}
|
||||
|
||||
public function getRefreshToken() : array
|
||||
{
|
||||
return $this->getAccessToken();
|
||||
}
|
||||
}
|
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;
|
||||
}
|
@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Import\Quickbooks\Repositories;
|
||||
|
||||
use App\Models\Company;
|
||||
use App\Libraries\MultiDB;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
class CompanyTokensRepository {
|
||||
|
||||
|
||||
private $company_key;
|
||||
private $store_key = "quickbooks-token";
|
||||
|
||||
public function __construct(string $key = null) {
|
||||
$this->company_key = $key ?? auth()->user->company()->company_key ?? null;
|
||||
$this->store_key .= $key;
|
||||
$this->setCompanyDbByKey();
|
||||
}
|
||||
|
||||
public function save(array $tokens) {
|
||||
$this->updateAccessToken($tokens['access_token'], $tokens['access_token_expires']);
|
||||
$this->updateRefreshToken($tokens['refresh_token'], $tokens['refresh_token_expires'], $tokens['realm']);
|
||||
}
|
||||
|
||||
|
||||
public function findByCompanyKey(): ?Company
|
||||
{
|
||||
return Company::where('company_key', $this->company_key)->first();
|
||||
}
|
||||
|
||||
public function setCompanyDbByKey()
|
||||
{
|
||||
MultiDB::findAndSetDbByCompanyKey($this->company_key);
|
||||
}
|
||||
|
||||
public function get() {
|
||||
return $this->getAccessToken() + $this->getRefreshToken();
|
||||
}
|
||||
|
||||
|
||||
protected function updateRefreshToken(string $token, string $expires, string $realm)
|
||||
{
|
||||
DB::table('companies')
|
||||
->where('company_key', $this->company_key)
|
||||
->update(['quickbooks_refresh_token' => $token,
|
||||
'quickbooks_realm_id' => $realm,
|
||||
'quickbooks_refresh_expires' => $expires ]);
|
||||
}
|
||||
|
||||
protected function updateAccessToken(string $token, string $expires )
|
||||
{
|
||||
|
||||
Cache::put([$this->store_key => $token], $expires);
|
||||
}
|
||||
|
||||
protected function getAccessToken( )
|
||||
{
|
||||
$result = Cache::get($this->store_key);
|
||||
|
||||
return $result ? ['access_token' => $result] : [];
|
||||
}
|
||||
|
||||
protected function getRefreshToken()
|
||||
{
|
||||
$result = (array) DB::table('companies')
|
||||
->select('quickbooks_refresh_token', 'quickbooks_realm_id')
|
||||
->where('company_key',$this->company_key)
|
||||
->where('quickbooks_refresh_expires','>',now())
|
||||
->first();
|
||||
|
||||
return $result? array_combine(['refresh_token','realm'], array_values($result) ) : [];
|
||||
}
|
||||
|
||||
}
|
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;
|
||||
}
|
||||
}
|
86
app/Services/Import/Quickbooks/Service.php
Normal file
86
app/Services/Import/Quickbooks/Service.php
Normal file
@ -0,0 +1,86 @@
|
||||
<?php
|
||||
namespace App\Services\Import\Quickbooks;
|
||||
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use App\Services\Import\Quickbooks\Auth;
|
||||
use App\Repositories\Import\Quickbooks\Contracts\RepositoryInterface;
|
||||
use App\Services\Import\QuickBooks\Contracts\SdkInterface as QuickbooksInterface;
|
||||
|
||||
final class Service
|
||||
{
|
||||
private QuickbooksInterface $sdk;
|
||||
|
||||
public function __construct(QuickbooksInterface $quickbooks) {
|
||||
$this->sdk = $quickbooks;
|
||||
}
|
||||
|
||||
public function getOAuth() : Auth
|
||||
{
|
||||
return new Auth($this->sdk);
|
||||
}
|
||||
|
||||
public function getAccessToken() : array
|
||||
{
|
||||
return $this->getOAuth()->getAccessToken();
|
||||
}
|
||||
|
||||
public function getRefreshToken() : array
|
||||
{
|
||||
// TODO: Check if token is Cached otherwise fetch a new one and Cache token and expire
|
||||
return $this->getAccessToken();
|
||||
}
|
||||
/**
|
||||
* fetch QuickBooks invoice records
|
||||
* @param int $max The maximum records to fetch. Default 100
|
||||
* @return Illuminate\Support\Collection;
|
||||
*/
|
||||
public function fetchInvoices(int $max = 100): Collection
|
||||
{
|
||||
return $this->fetchRecords('Invoice', $max) ;
|
||||
}
|
||||
|
||||
/**
|
||||
* fetch QuickBooks payment records
|
||||
* @param int $max The maximum records to fetch. Default 100
|
||||
* @return Illuminate\Support\Collection;
|
||||
*/
|
||||
public function fetchPayments(int $max = 100): Collection
|
||||
{
|
||||
return $this->fetchRecords('Payment', $max) ;
|
||||
}
|
||||
|
||||
/**
|
||||
* fetch QuickBooks product records
|
||||
* @param int $max The maximum records to fetch. Default 100
|
||||
* @return Illuminate\Support\Collection;
|
||||
*/
|
||||
public function fetchItems(int $max = 100): Collection
|
||||
{
|
||||
return $this->fetchRecords('Item', $max) ;
|
||||
}
|
||||
|
||||
protected function fetchRecords(string $entity, $max = 100) : Collection {
|
||||
return (self::RepositoryFactory($entity))->get($max);
|
||||
}
|
||||
|
||||
private static function RepositoryFactory(string $entity) : RepositoryInterface
|
||||
{
|
||||
return app("\\App\\Repositories\\Import\Quickbooks\\{$entity}Repository");
|
||||
}
|
||||
|
||||
/**
|
||||
* fetch QuickBooks customer records
|
||||
* @param int $max The maximum records to fetch. Default 100
|
||||
* @return Illuminate\Support\Collection;
|
||||
*/
|
||||
public function fetchCustomers(int $max = 100): Collection
|
||||
{
|
||||
return $this->fetchRecords('Customer', $max) ;
|
||||
}
|
||||
|
||||
public function totalRecords(string $entity) : int
|
||||
{
|
||||
return (self::RepositoryFactory($entity))->count();
|
||||
}
|
||||
}
|
@ -63,6 +63,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",
|
||||
@ -85,6 +86,7 @@
|
||||
"predis/predis": "^2",
|
||||
"psr/http-message": "^1.0",
|
||||
"pusher/pusher-php-server": "^7.2",
|
||||
"quickbooks/v3-php-sdk": "6.1.4-alpha",
|
||||
"razorpay/razorpay": "2.*",
|
||||
"sentry/sentry-laravel": "^4",
|
||||
"setasign/fpdf": "^1.8",
|
||||
@ -197,6 +199,10 @@
|
||||
{
|
||||
"type": "vcs",
|
||||
"url": "https://github.com/turbo124/snappdf"
|
||||
},
|
||||
{
|
||||
"type":"vcs",
|
||||
"url":"https://github.com/karneaud/QuickBooks-V3-PHP-SDK.git"
|
||||
}
|
||||
],
|
||||
"minimum-stability": "dev",
|
||||
|
130
composer.lock
generated
130
composer.lock
generated
@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "95e7bd229644d1d8e768ecfbc78582cd",
|
||||
"content-hash": "9e7ea46cfef2848f4eac13cc9c0c679a",
|
||||
"packages": [
|
||||
{
|
||||
"name": "adrienrn/php-mimetyper",
|
||||
@ -4048,6 +4048,68 @@
|
||||
},
|
||||
"time": "2024-07-22T02:40:27+00:00"
|
||||
},
|
||||
{
|
||||
"name": "invoiceninja/inspector",
|
||||
"version": "v3.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/invoiceninja/inspector.git",
|
||||
"reference": "29bc1ee7ae9d967287ecbd3485a2fee41a13e65f"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/invoiceninja/inspector/zipball/29bc1ee7ae9d967287ecbd3485a2fee41a13e65f",
|
||||
"reference": "29bc1ee7ae9d967287ecbd3485a2fee41a13e65f",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"doctrine/dbal": "^4.0",
|
||||
"illuminate/support": "^11.0",
|
||||
"php": "^8.2"
|
||||
},
|
||||
"require-dev": {
|
||||
"orchestra/testbench": "^9.1",
|
||||
"phpunit/phpunit": "^11.1"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"providers": [
|
||||
"InvoiceNinja\\Inspector\\InspectorServiceProvider"
|
||||
],
|
||||
"aliases": {
|
||||
"Inspector": "InvoiceNinja\\Inspector\\InspectorFacade"
|
||||
}
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"InvoiceNinja\\Inspector\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Benjamin Beganović",
|
||||
"email": "benjamin.beganovic4@outlook.com",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "Simplified database records management",
|
||||
"homepage": "https://github.com/invoiceninja/inspector",
|
||||
"keywords": [
|
||||
"inspector",
|
||||
"invoiceninja"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/invoiceninja/inspector/issues",
|
||||
"source": "https://github.com/invoiceninja/inspector/tree/v3.0"
|
||||
},
|
||||
"time": "2024-06-04T12:31:47+00:00"
|
||||
},
|
||||
{
|
||||
"name": "invoiceninja/ubl_invoice",
|
||||
"version": "v2.2.2",
|
||||
@ -9089,6 +9151,72 @@
|
||||
},
|
||||
"time": "2023-12-15T10:58:53+00:00"
|
||||
},
|
||||
{
|
||||
"name": "quickbooks/v3-php-sdk",
|
||||
"version": "v6.1.4-alpha",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/karneaud/QuickBooks-V3-PHP-SDK.git",
|
||||
"reference": "89ff2b6dcfc94634cf5806cacda1286a6898249f"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/karneaud/QuickBooks-V3-PHP-SDK/zipball/89ff2b6dcfc94634cf5806cacda1286a6898249f",
|
||||
"reference": "89ff2b6dcfc94634cf5806cacda1286a6898249f",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-dom": "*",
|
||||
"ext-mbstring": "*",
|
||||
"php": ">=5.6.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"php-mock/php-mock": "^2.3",
|
||||
"php-mock/php-mock-phpunit": "^2.6",
|
||||
"phpunit/phpunit": "^5.0 || ^6.0 || ^7.0 || ^8"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-curl": "Uses Curl to make HTTP Requests",
|
||||
"guzzlehttp/guzzle": "Uses Guzzle to make HTTP Requests"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"QuickBooksOnline\\API\\": "src/"
|
||||
}
|
||||
},
|
||||
"archive": {
|
||||
"exclude": [
|
||||
"/docs",
|
||||
"/src/Utility.Test",
|
||||
"/src/XSD2PHP/docs",
|
||||
"/src/XSD2PHP/test",
|
||||
"/test"
|
||||
]
|
||||
},
|
||||
"license": [
|
||||
"Apache-2.0"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "abisalehalliprasan",
|
||||
"email": "anil_kumar3@intuit.com"
|
||||
}
|
||||
],
|
||||
"description": "The Official PHP SDK for QuickBooks Online Accounting API",
|
||||
"homepage": "http://developer.intuit.com",
|
||||
"keywords": [
|
||||
"api",
|
||||
"http",
|
||||
"quickbooks",
|
||||
"rest",
|
||||
"smallbusiness"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/karneaud/QuickBooks-V3-PHP-SDK/tree/v6.1.4-alpha"
|
||||
},
|
||||
"time": "2024-08-16T01:21:19+00:00"
|
||||
},
|
||||
{
|
||||
"name": "ralouphie/getallheaders",
|
||||
"version": "3.0.3",
|
||||
|
@ -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(),
|
||||
|
||||
];
|
||||
|
@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('companies', function (Blueprint $table) {
|
||||
$table->string('quickbooks_realm_id')->nullable();
|
||||
$table->string('quickbooks_refresh_token')->nullable();
|
||||
$table->dateTime('quickbooks_refresh_expires')->nullable();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('companies', function (Blueprint $table) {
|
||||
$table->dropColumn(['quickbooks_realm_id', 'quickbooks_refresh_token','quickbooks_refresh_expires']);
|
||||
});
|
||||
}
|
||||
};
|
@ -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');
|
||||
|
@ -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;
|
||||
|
@ -0,0 +1,126 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Http\Controllers;
|
||||
|
||||
use App\Services\Import\Quickbooks\Contracts\SdkInterface as QuickbooksInterface;
|
||||
use App\Services\Import\Quickbooks\Service as QuickbooksService;
|
||||
use App\Services\Import\Quickbooks\SdkWrapper as QuickbooksSDK;
|
||||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Foundation\Testing\WithFaker;
|
||||
use Illuminate\Support\Facades\Session;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Bus;
|
||||
use GuzzleHttp\Psr7\Message;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Str;
|
||||
use Mockery\MockInterface;
|
||||
use Tests\MockAccountData;
|
||||
use Tests\TestCase;
|
||||
use Mockery;
|
||||
|
||||
class ImportQuickbooksControllerTest extends TestCase
|
||||
{
|
||||
use MockAccountData;
|
||||
use DatabaseTransactions;
|
||||
|
||||
private $mock;
|
||||
private $state;
|
||||
|
||||
protected function setUp(): void {
|
||||
|
||||
parent::setUp();
|
||||
|
||||
$this->state = Str::random(4);
|
||||
$this->mock = Mockery::mock(stdClass::class);
|
||||
$this->makeTestData();
|
||||
|
||||
Session::start();
|
||||
|
||||
//app()->singleton(QuickbooksInterface::class, fn() => new QuickbooksSDK($this->mock));
|
||||
}
|
||||
|
||||
public function testAuthorize(): void
|
||||
{
|
||||
|
||||
$this->mock->shouldReceive('getState')->andReturn($this->state);
|
||||
$this->mock->shouldReceive('getAuthorizationCodeURL')->andReturn('https://example.com');
|
||||
$this->mock->shouldReceive("getOAuth2LoginHelper")->andReturn($this->mock);
|
||||
|
||||
Cache::spy();
|
||||
Cache::shouldReceive('get')
|
||||
->with($token = $this->company->company_key)
|
||||
->andReturn( ['company_key' => $token, 'id' => $this->company->id]);
|
||||
Cache::shouldReceive('has')
|
||||
->andReturn(true);
|
||||
// Perform the test
|
||||
$response = $this->get(route('authorize.quickbooks', ['token' => $token]));
|
||||
$response->assertStatus(302);
|
||||
|
||||
Cache::shouldHaveReceived('put')->once()->with($this->state, $token, 90);
|
||||
}
|
||||
|
||||
public function testOnAuthorized(): void
|
||||
{
|
||||
$token = ['company_key' => $this->company->company_key, 'id' => $this->company->id] ;
|
||||
|
||||
$this->mock->shouldReceive('getAccessToken')->andReturn(Mockery::mock(stdClass::class,function(MockInterface $mock){
|
||||
$mock->shouldReceive('getAccessToken')->andReturn('abcdefg');
|
||||
$mock->shouldReceive('getRefreshToken')->andReturn('abcdefghi');
|
||||
$mock->shouldReceive('getAccessTokenExpiresAt')->andReturn(3600);
|
||||
$mock->shouldReceive('getRefreshTokenExpiresAt')->andReturn(8726400);
|
||||
}));
|
||||
$this->mock->shouldReceive("getOAuth2LoginHelper")->andReturn($this->mock);
|
||||
$this->mock->shouldReceive('exchangeAuthorizationCodeForToken')->once();
|
||||
|
||||
Cache::spy();
|
||||
Cache::shouldReceive('has')
|
||||
->andReturn(true);
|
||||
Cache::shouldReceive('get')->andReturn($token);
|
||||
Cache::shouldReceive('pull')->andReturn($token['company_key']);
|
||||
// Perform the test
|
||||
$response = $this->get("/quickbooks/authorized/?code=123456&state={$this->state}&realmId=12345678");
|
||||
$response->assertStatus(200);
|
||||
|
||||
Cache::shouldHaveReceived('put')->once()->with($token['company_key'], 'abcdefg', 3600);
|
||||
|
||||
$this->mock->shouldHaveReceived('exchangeAuthorizationCodeForToken')->once()->with(123456,12345678);
|
||||
}
|
||||
|
||||
public function testImport(): void
|
||||
{
|
||||
// Cache::spy();
|
||||
//Bus::fake();
|
||||
$data = $this->setUpTestData('customers');
|
||||
$count = count($data);
|
||||
$this->mock->shouldReceive('Query')->andReturnUsing(
|
||||
function($val, $s = 1, $max = 1000) use ($count, $data) {
|
||||
if(stristr($val, 'count')) {
|
||||
return $count;
|
||||
}
|
||||
|
||||
return Arr::take($data,$max);
|
||||
}
|
||||
);
|
||||
|
||||
// Perform the test
|
||||
$response = $this->actingAs($this->user)->withHeaders([
|
||||
'X-API-TOKEN' => $this->token,
|
||||
])->post('/api/v1/import/quickbooks',[
|
||||
'import_types' => ['client']
|
||||
]);
|
||||
$response->assertStatus(200);
|
||||
|
||||
//Cache::shouldHaveReceived('has')->once()->with("{$hash}-client");
|
||||
//Bus::assertDispatched(\App\Jobs\Import\QuickbooksIngest::class);
|
||||
}
|
||||
|
||||
protected function setUpTestData($file) {
|
||||
$data = json_decode(
|
||||
file_get_contents(base_path("tests/Mock/Quickbooks/Data/$file.json")),true
|
||||
);
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
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_types' => ['client'],
|
||||
], $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