Migrate commits from 2-migration-with-json into v2 (#3241)

* Scaffold test case

* Import.php tests:
- Basic test scaffold
- Test if exception is thrown when unknown resource
- Company update test

* Migration importer & exception classes

* Company migration test
- Added 3rd parameter for accepting custom resources
- Wip tax_rates migration

* Tax rate migration

* Tax rate update
- Added company_id & user_id property modifiers

* Users migration

* Save IDs for users importing

* Add 'transformIds' method

* Importing clients
- An exception for resource not migration
- Dependency logic
- Removing id on insert

* Exception for unresolved dependency

* Import clients

* Method for inspecting user_id

* Importing invoices

* Importing quotes

* Fix tests & wrap with try-catch

* Fix tax_rates user_id transform

* Working on migration

* Tests for migration

* fixes for test

* Tests for Import.php
- Added ext-json to composer.json

* Tests for Import.php
- Added ext-json to composer.json

* Change migration exceptions to MigrationValidatorFailed

* Fixes for tests and counters

* Unzipping the migration archive
- Changed .gitignore to ignore all local migrations

* Comparing local data with inserted

* Ignore verification - wip

* Fix formatting for api.php

* Uploading file test (wip)

* Fix typo

Co-authored-by: David Bomba <turbo124@gmail.com>
This commit is contained in:
Benjamin Beganović 2020-01-23 21:35:00 +01:00 committed by David Bomba
parent 67da87b94f
commit 11cc40d23a
18 changed files with 1473 additions and 65 deletions

5
.gitignore vendored
View File

@ -21,4 +21,7 @@ yarn-error.log
.env.dusk.local
/public/vendors/*
public/mix-manifest.json
*.log
*.log
# Ignore local migrations
storage/migrations

View File

@ -0,0 +1,19 @@
<?php
namespace App\Exceptions;
use Exception;
use Throwable;
class MigrationValidatorFailed extends Exception
{
public function __construct($message = "", $code = 0, Throwable $previous = null)
{
parent::__construct($message, $code, $previous);
}
public function report()
{
// Send, an e-mail & notify users.
}
}

View File

@ -0,0 +1,28 @@
<?php
namespace App\Exceptions;
use Exception;
use Throwable;
class ProcessingMigrationArchiveFailed extends Exception
{
/**
* @var Throwable
*/
private $previous;
public function __construct($message = "", $code = 0, Throwable $previous = null)
{
parent::__construct($message, $code, $previous);
$this->message = $message;
$this->code = $code;
$this->previous = $previous;
}
public function report()
{
return 'Unable to open migration archive.';
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace App\Exceptions;
use Exception;
use Throwable;
class ResourceDependencyMissing extends Exception
{
private $resource;
private $dependency;
public function __construct($resource, $dependency)
{
parent::__construct();
$this->resource = $resource;
$this->dependency = $dependency;
}
public function report()
{
return "Resource '{$this->resource}' depends on '{$this->dependency}'.";
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace App\Exceptions;
use Exception;
use Throwable;
class ResourceNotAvailableForMigration extends Exception
{
private $resource;
public function __construct($resource)
{
parent::__construct();
$this->resource = $resource;
}
public function report()
{
// TODO: Handle this nicely, throw response, etc.
return "Resource {$this->resource} is not available for the migration.";
}
}

View File

@ -12,7 +12,9 @@
namespace App\Http\Controllers;
use App\Http\Requests\Account\CreateAccountRequest;
use App\Http\Requests\Migration\UploadMigrationFileRequest;
use App\Jobs\Account\CreateAccount;
use App\Jobs\Util\StartMigration;
use App\Models\Account;
use App\Models\Company;
use App\Models\CompanyUser;
@ -136,4 +138,16 @@ class MigrationController extends BaseController
return response()->json(['message'=>'Settings preserved'], 200);
}
public function uploadMigrationFile(UploadMigrationFileRequest $request)
{
$file = $request->file('migration')->storeAs(
'migrations', $request->file('migration')->getClientOriginalName()
);
/** Not tested. */
StartMigration::dispatchNow($file, auth()->user(), auth()->user()->company);
return response()->json([], 200);
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace App\Http\Requests\Migration;
use Illuminate\Foundation\Http\FormRequest;
class UploadMigrationFileRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return auth()->user()->isAdmin();
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'migration' => ['required', 'mimes:zip'],
];
}
}

552
app/Jobs/Util/Import.php Normal file
View File

@ -0,0 +1,552 @@
<?php
namespace App\Jobs\Util;
use App\Exceptions\MigrationValidatorFailed;
use App\Exceptions\ResourceDependencyMissing;
use App\Exceptions\ResourceNotAvailableForMigration;
use App\Factory\ClientFactory;
use App\Factory\CreditFactory;
use App\Factory\InvoiceFactory;
use App\Factory\PaymentFactory;
use App\Factory\ProductFactory;
use App\Factory\QuoteFactory;
use App\Factory\TaxRateFactory;
use App\Factory\UserFactory;
use App\Http\Requests\Company\UpdateCompanyRequest;
use App\Http\ValidationRules\ValidUserForCompany;
use App\Jobs\Company\CreateCompanyToken;
use App\Libraries\MultiDB;
use App\Models\Client;
use App\Models\Company;
use App\Models\Credit;
use App\Models\Invoice;
use App\Models\Payment;
use App\Models\Product;
use App\Models\Quote;
use App\Models\TaxRate;
use App\Models\User;
use App\Repositories\ClientContactRepository;
use App\Repositories\ClientRepository;
use App\Repositories\CompanyRepository;
use App\Repositories\CreditRepository;
use App\Repositories\InvoiceRepository;
use App\Repositories\PaymentRepository;
use App\Repositories\ProductRepository;
use App\Repositories\QuoteRepository;
use App\Repositories\UserRepository;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Str;
class Import implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
/**
* @var array
*/
private $data;
/**
* @var Company
*/
private $company;
/**
* @var array
*/
private $available_imports = [
'company', 'users', 'tax_rates', 'clients', 'products', 'invoices', 'quotes', 'payments', 'credits',
];
/**
* @var User
*/
private $user;
/**
* Custom list of resources to be imported.
*
* @var array
*/
private $resources;
/**
* Local state manager for ids.
*
* @var array
*/
private $ids = [];
/**
* Create a new job instance.
*
* @param array $data
* @param Company $company
* @param User $user
* @param array $resources
*/
public function __construct(array $data, Company $company, User $user, array $resources = [])
{
$this->data = $data;
$this->company = $company;
$this->user = $user;
$this->resources = $resources;
}
/**
* Execute the job.
*
* @return void
* @throws \Exception
*/
public function handle()
{
foreach ($this->data as $key => $resource) {
if (!in_array($key, $this->available_imports)) {
throw new ResourceNotAvailableForMigration($key);
}
$method = sprintf("process%s", Str::ucfirst(Str::camel($key)));
$this->{$method}($resource);
}
}
/**
* @param array $data
* @throws \Exception
*/
private function processCompany(array $data): void
{
Company::unguard();
$rules = (new UpdateCompanyRequest())->rules();
$validator = Validator::make($data, $rules);
if ($validator->fails()) {
throw new MigrationValidatorFailed($validator->errors());
}
if(isset($data['account_id']))
unset($data['account_id']);
$company_repository = new CompanyRepository();
$company_repository->save($data, $this->company);
}
/**
* @param array $data
* @throws \Exception
*/
private function processTaxRates(array $data): void
{
TaxRate::unguard();
$rules = [
'*.name' => 'required',
//'*.name' => 'required|distinct|unique:tax_rates,name,null,null,company_id,' . $this->company->id,
'*.rate' => 'required|numeric',
];
$validator = Validator::make($data, $rules);
if ($validator->fails()) {
throw new MigrationValidatorFailed($validator->errors());
}
foreach ($data as $resource) {
$modified = $resource;
$company_id = $this->company->id;
$user_id = $this->processUserId($resource);
if(isset($resource['user_id']))
unset($resource['user_id']);
if(isset($resource['company_id']))
unset($resource['company_id']);
$tax_rate = TaxRateFactory::create($this->company->id, $user_id);
$tax_rate->fill($resource);
$tax_rate->save();
}
}
/**
* @param array $data
* @throws \Exception
*/
private function processUsers(array $data): void
{
User::unguard();
$rules = [
'*.first_name' => ['string'],
'*.last_name' => ['string'],
'*.email' => ['distinct'],
];
// if (config('ninja.db.multi_db_enabled')) {
// array_push($rules['*.email'], new ValidUserForCompany());
// }
$validator = Validator::make($data, $rules);
if ($validator->fails()) {
throw new MigrationValidatorFailed($validator->errors());
}
$user_repository = new UserRepository();
foreach ($data as $resource) {
$modified = $resource;
unset($modified['id']);
$user = $user_repository->save($modified, $this->fetchUser($resource['email']));
$user_agent = array_key_exists('token_name', $resource) ?: request()->server('HTTP_USER_AGENT');
CreateCompanyToken::dispatchNow($this->company, $user, $user_agent);
$key = "users_{$resource['id']}";
$this->ids['users'][$key] = [
'old' => $resource['id'],
'new' => $user->id,
];
}
}
/**
* @param array $data
* @throws ResourceDependencyMissing
* @throws \Exception
*/
private function processClients(array $data): void
{
Client::unguard();
$client_repository = new ClientRepository(new ClientContactRepository());
foreach ($data as $key => $resource) {
$modified = $resource;
$modified['company_id'] = $this->company->id;
$modified['user_id'] = $this->processUserId($resource);
unset($modified['id']);
$client = $client_repository->save($modified, ClientFactory::create(
$this->company->id, $modified['user_id'])
);
$key = "clients_{$resource['id']}";
$this->ids['clients'][$key] = [
'old' => $resource['id'],
'new' => $client->id,
];
}
}
private function processProducts(array $data): void
{
Product::unguard();
$rules = [
//'*.product_key' => 'required|distinct|unique:products,product_key,null,null,company_id,' . $this->company->id,
'*.cost' => 'numeric',
'*.price' => 'numeric',
'*.quantity' => 'numeric',
];
$validator = Validator::make($data, $rules);
if ($validator->fails()) {
throw new MigrationValidatorFailed($validator->errors());
}
$product_repository = new ProductRepository();
foreach ($data as $resource) {
$modified = $resource;
$modified['company_id'] = $this->company->id;
$modified['user_id'] = $this->processUserId($resource);
unset($modified['id']);
$product_repository->save($modified, ProductFactory::create(
$this->company->id, $modified['user_id'])
);
}
}
private function processInvoices(array $data): void
{
Invoice::unguard();
$rules = [
'*.client_id' => ['required'],
];
$validator = Validator::make($data, $rules);
if ($validator->fails()) {
throw new MigrationValidatorFailed($validator->errors());
}
$invoice_repository = new InvoiceRepository();
foreach ($data as $resource) {
$modified = $resource;
if (array_key_exists('client_id', $resource) && !array_key_exists('clients', $this->ids)) {
throw new ResourceDependencyMissing(array_key_first($data), 'clients');
}
$modified['client_id'] = $this->transformId('clients', $resource['client_id']);
$modified['user_id'] = $this->processUserId($resource);
$modified['company_id'] = $this->company->id;
unset($modified['id']);
$invoice = $invoice_repository->save(
$modified, InvoiceFactory::create($this->company->id, $modified['user_id'])
);
$key = "invoices_{$resource['id']}";
$this->ids['invoices'][$key] = [
'old' => $resource['id'],
'new' => $invoice->id,
];
}
}
private function processCredits(array $data): void
{
Credit::unguard();
$rules = [
'*.client_id' => ['required'],
];
$validator = Validator::make($data, $rules);
if ($validator->fails()) {
throw new MigrationValidatorFailed($validator->errors());
}
$credit_repository = new CreditRepository();
foreach ($data as $resource) {
$modified = $resource;
if (array_key_exists('client_id', $resource) && !array_key_exists('clients', $this->ids)) {
throw new ResourceDependencyMissing(array_key_first($data), 'clients');
}
$modified['client_id'] = $this->transformId('clients', $resource['client_id']);
$modified['user_id'] = $this->processUserId($resource);
$modified['company_id'] = $this->company->id;
unset($modified['id']);
$invoice = $credit_repository->save(
$modified, CreditFactory::create($this->company->id, $modified['user_id'])
);
$key = "credits_{$resource['id']}";
$this->ids['credits'][$key] = [
'old' => $resource['id'],
'new' => $invoice->id,
];
}
}
private function processQuotes(array $data): void
{
Quote::unguard();
$rules = [
'*.client_id' => ['required'],
];
$validator = Validator::make($data, $rules);
if ($validator->fails()) {
throw new MigrationValidatorFailed($validator->errors());
}
$quote_repository = new QuoteRepository();
foreach ($data as $resource) {
$modified = $resource;
if (array_key_exists('client_id', $resource) && !array_key_exists('clients', $this->ids)) {
throw new ResourceDependencyMissing(array_key_first($data), 'clients');
}
$modified['client_id'] = $this->transformId('clients', $resource['client_id']);
$modified['user_id'] = $this->processUserId($resource);
$modified['company_id'] = $this->company->id;
unset($modified['id']);
$invoice = $quote_repository->save(
$modified, QuoteFactory::create($this->company->id, $modified['user_id'])
);
$old_user_key = array_key_exists('user_id', $resource) ?? $this->user->id;
$this->ids['quotes'] = [
"quotes_{$old_user_key}" => [
'old' => $old_user_key,
'new' => $invoice->id,
]
];
}
}
private function processPayments(array $data): void
{
Payment::reguard();
$rules = [
'*.amount' => ['required'],
'*.client_id' => ['required'],
];
$validator = Validator::make($data, $rules);
if ($validator->fails()) {
throw new MigrationValidatorFailed($validator->errors());
}
$payment_repository = new PaymentRepository(new CreditRepository());
foreach ($data as $resource) {
$modified = $resource;
if (array_key_exists('client_id', $resource) && !array_key_exists('clients', $this->ids)) {
throw new ResourceDependencyMissing(array_key_first($data), 'clients');
}
$modified['client_id'] = $this->transformId('clients', $resource['client_id']);
$modified['user_id'] = $this->processUserId($resource);
//$modified['invoice_id'] = $this->transformId('invoices', $resource['invoice_id']);
$modified['company_id'] = $this->company->id;
//unset($modified['invoices']);
unset($modified['invoice_id']);
if(isset($modified['invoices']))
{
foreach($modified['invoices'] as $invoice)
$invoice['invoice_id'] = $this->transformId('invoices', $invoice['invoice_id']);
}
$payment = $payment_repository->save(
$modified, PaymentFactory::create($this->company->id, $modified['user_id'])
);
$old_user_key = array_key_exists('user_id', $resource) ?? $this->user->id;
$this->ids['payments'] = [
"payments_{$old_user_key}" => [
'old' => $old_user_key,
'new' => $payment->id,
]
];
}
}
/**
* |--------------------------------------------------------------------------
* | Additional migration methods.
* |--------------------------------------------------------------------------
* |
* | These methods aren't initialized automatically, so they don't depend on
* | the migration data.
*/
/**
* Cloned from App\Http\Requests\User\StoreUserRequest.
*
* @param string $data
* @return User
*/
public function fetchUser(string $data): User
{
$user = MultiDB::hasUser(['email' => $data]);
if (!$user) {
$user = UserFactory::create();
}
return $user;
}
/**
* @param string $resource
* @param string $old
* @return int
* @throws \Exception
*/
public function transformId(string $resource, string $old): int
{
if (!array_key_exists($resource, $this->ids)) {
throw new \Exception("Resource {$resource} not available.");
}
if (!array_key_exists("{$resource}_{$old}", $this->ids[$resource])) {
throw new \Exception("Missing resource key: {$resource}_{$old}");
}
return $this->ids[$resource]["{$resource}_{$old}"]['new'];
}
/**
* Process & handle user_id
*
* @param array $resource
* @return int|mixed
* @throws \Exception
*/
public function processUserId(array $resource)
{
if (!array_key_exists('user_id', $resource)) {
return $this->user->id;
}
if (array_key_exists('user_id', $resource) && !array_key_exists('users', $this->ids)) {
return $this->user->id;
}
return $this->transformId('users', $resource['user_id']);
}
}

View File

@ -0,0 +1,70 @@
<?php
namespace App\Jobs\Util;
use App\Exceptions\ProcessingMigrationArchiveFailed;
use App\Models\Company;
use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class StartMigration implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
private $filepath;
/**
* @var User
*/
private $user;
/**
* @var Company
*/
private $company;
/**
* Create a new job instance.
*
* @param $filepath
* @param User $user
* @param Company $company
*/
public function __construct($filepath, User $user, Company $company)
{
$this->filepath = $filepath;
$this->user = $user;
$this->company = $company;
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
$zip = new \ZipArchive();
$archive = $zip->open($this->filepath);
$filename = pathinfo($this->filepath, PATHINFO_FILENAME);
try {
if ($archive) {
$zip->extractTo(storage_path("migrations/{$filename}"));
$zip->close();
} else {
throw new ProcessingMigrationArchiveFailed();
}
} catch (ProcessingMigrationArchiveFailed $e) {
// TODO: Break the code, stop the migration.. send an e-mail.
}
// Rest of the migration..
}
}

View File

@ -69,11 +69,13 @@ class PaymentRepository extends BaseRepository
private function applyPayment(array $data, Payment $payment): ?Payment
{
$payment->fill($data);
$payment->status_id = Payment::STATUS_COMPLETED;
$payment->save();
if (!$payment->number)
$payment->number = $payment->client->getNextPaymentNumber($payment->client);
@ -87,12 +89,12 @@ class PaymentRepository extends BaseRepository
$invoice_totals = array_sum(array_column($data['invoices'], 'amount'));
$invoices = Invoice::whereIn('id', array_column($data['invoices'], 'invoice_id'))->company()->get();
$invoices = Invoice::whereIn('id', array_column($data['invoices'], 'invoice_id'))->get();
$payment->invoices()->saveMany($invoices);
foreach ($data['invoices'] as $paid_invoice) {
$invoice = Invoice::whereId($paid_invoice['invoice_id'])->company()->first();
$invoice = Invoice::whereId($paid_invoice['invoice_id'])->first();
if ($invoice) {
ApplyInvoicePayment::dispatchNow($invoice, $payment, $paid_invoice['amount'], $invoice->company);
@ -107,12 +109,12 @@ class PaymentRepository extends BaseRepository
$credit_totals = array_sum(array_column($data['credits'], 'amount'));
$credits = Credit::whereIn('id', array_column($data['credits'], 'credit_id'))->company()->get();
$credits = Credit::whereIn('id', array_column($data['credits'], 'credit_id'))->get();
$payment->credits()->saveMany($credits);
foreach ($data['credits'] as $paid_credit) {
$credit = Credit::whereId($paid_credit['credit_id'])->company()->first();
$credit = Credit::whereId($paid_credit['credit_id'])->first();
if ($credit)
ApplyCreditPayment::dispatchNow($paid_credit, $payment, $paid_credit['amount'], $credit->company);
@ -145,7 +147,7 @@ class PaymentRepository extends BaseRepository
foreach ($data['invoices'] as $adjusted_invoice) {
$invoice = Invoice::whereId($adjusted_invoice['invoice_id'])->company()->first();
$invoice = Invoice::whereId($adjusted_invoice['invoice_id'])->first();
$invoice_total_adjustment += $adjusted_invoice['amount'];
@ -154,7 +156,7 @@ class PaymentRepository extends BaseRepository
//process and insert credit notes
foreach ($adjusted_invoice['credits'] as $credit) {
$credit = $this->credit_repo->save($credit, CreditFactory::create(auth()->user()->company()->id, auth()->user()->id), $invoice);
$credit = $this->credit_repo->save($credit, CreditFactory::create(auth()->user()->id, auth()->user()->id), $invoice);
}

View File

@ -45,7 +45,6 @@ class SystemHealth
$system_health = false;
}
return [
'system_health' => $system_health,
'extensions' => self::extensions(),
@ -91,7 +90,9 @@ class SystemHealth
}
}
return $result;
}
return $result;
}
}

View File

@ -81,11 +81,24 @@ trait GeneratesCounter
$this->resetCounters($client);
//todo handle if we have specific client patterns in the future
$pattern = $client->company->settings->credit_number_pattern;
$prefix = $client->company->settings->credit_number_pattern;
$padding = $client->company->settings->credit_number_pattern;
$pattern = $client->getSetting('credit_number_pattern');
//Determine if we are using client_counters
if (strpos($pattern, 'clientCounter')) {
$counter = $client->settings->credit_number_counter;
$counter_entity = $client;
} elseif (strpos($pattern, 'groupCounter')) {
$counter = $client->group_settings->credit_number_counter;
$counter_entity = $client->group_settings;
} else {
$counter = $client->company->settings->credit_number_counter;
$counter_entity = $client->company;
}
$credit_number = $this->checkEntityNumber(Credit::class, $client, $counter, $padding, $prefix, $pattern);
//Return a valid counter
$pattern = $client->getSetting('credit_number_pattern');
$padding = $client->getSetting('counter_padding');
$credit_number = $this->checkEntityNumber(Credit::class, $client, $counter, $padding, $pattern);
$this->incrementCounter($client->company, 'credit_number_counter');

View File

@ -60,7 +60,8 @@
"laravel/dusk": "^5.0",
"mockery/mockery": "^1.0",
"nunomaduro/collision": "^2.0",
"phpunit/phpunit": "^7.0"
"phpunit/phpunit": "^7.0",
"ext-json": "*"
},
"autoload": {
"classmap": [

View File

@ -20,105 +20,108 @@ Route::middleware('auth:api')->get('/user', function (Request $request) {
Route::group(['middleware' => ['api_secret_check']], function () {
Route::post('api/v1/signup', 'AccountController@store')->name('signup.submit');
Route::post('api/v1/oauth_login', 'Auth\LoginController@oauthApiLogin');
Route::post('api/v1/signup', 'AccountController@store')->name('signup.submit');
Route::post('api/v1/oauth_login', 'Auth\LoginController@oauthApiLogin');
});
Route::group(['api_secret_check','email_db'], function () {
Route::group(['api_secret_check', 'email_db'], function () {
Route::post('api/v1/login', 'Auth\LoginController@apiLogin')->name('login.submit');
Route::post('api/v1/reset_password', 'Auth\ForgotPasswordController@sendResetLinkEmail')->name('password.reset');
Route::post('api/v1/login', 'Auth\LoginController@apiLogin')->name('login.submit');
Route::post('api/v1/reset_password', 'Auth\ForgotPasswordController@sendResetLinkEmail')->name('password.reset');
});
Route::group(['middleware' => ['api_db','api_secret_check','token_auth','locale'], 'prefix' =>'api/v1', 'as' => 'api.'], function () {
Route::group(['middleware' => ['api_db', 'api_secret_check', 'token_auth', 'locale'], 'prefix' => 'api/v1', 'as' => 'api.'], function () {
Route::resource('activities', 'ActivityController'); // name = (clients. index / create / show / update / destroy / edit
Route::resource('activities', 'ActivityController'); // name = (clients. index / create / show / update / destroy / edit
Route::resource('clients', 'ClientController'); // name = (clients. index / create / show / update / destroy / edit
Route::resource('clients', 'ClientController'); // name = (clients. index / create / show / update / destroy / edit
Route::post('clients/bulk', 'ClientController@bulk')->name('clients.bulk');
Route::post('clients/bulk', 'ClientController@bulk')->name('clients.bulk');
Route::resource('invoices', 'InvoiceController'); // name = (invoices. index / create / show / update / destroy / edit
Route::resource('invoices', 'InvoiceController'); // name = (invoices. index / create / show / update / destroy / edit
Route::get('invoices/{invoice}/{action}', 'InvoiceController@action')->name('invoices.action');
Route::get('invoices/{invoice}/{action}', 'InvoiceController@action')->name('invoices.action');
Route::post('invoices/bulk', 'InvoiceController@bulk')->name('invoices.bulk');
Route::post('invoices/bulk', 'InvoiceController@bulk')->name('invoices.bulk');
Route::resource('products', 'ProductController'); // name = (products. index / create / show / update / destroy / edit
Route::resource('products', 'ProductController'); // name = (products. index / create / show / update / destroy / edit
Route::post('products/bulk', 'ProductController@bulk')->name('products.bulk');
Route::post('products/bulk', 'ProductController@bulk')->name('products.bulk');
Route::resource('quotes', 'QuoteController'); // name = (quotes. index / create / show / update / destroy / edit
Route::resource('quotes', 'QuoteController'); // name = (quotes. index / create / show / update / destroy / edit
Route::post('quotes/bulk', 'QuoteController@bulk')->name('quotes.bulk');
Route::post('quotes/bulk', 'QuoteController@bulk')->name('quotes.bulk');
Route::resource('recurring_invoices', 'RecurringInvoiceController'); // name = (recurring_invoices. index / create / show / update / destroy / edit
Route::resource('recurring_invoices', 'RecurringInvoiceController'); // name = (recurring_invoices. index / create / show / update / destroy / edit
Route::post('recurring_invoices/bulk', 'RecurringInvoiceController@bulk')->name('recurring_invoices.bulk');
Route::post('recurring_invoices/bulk', 'RecurringInvoiceController@bulk')->name('recurring_invoices.bulk');
Route::resource('recurring_quotes', 'RecurringQuoteController'); // name = (recurring_invoices. index / create / show / update / destroy / edit
Route::resource('recurring_quotes', 'RecurringQuoteController'); // name = (recurring_invoices. index / create / show / update / destroy / edit
Route::post('recurring_quotes/bulk', 'RecurringQuoteController@bulk')->name('recurring_quotes.bulk');
Route::post('recurring_quotes/bulk', 'RecurringQuoteController@bulk')->name('recurring_quotes.bulk');
Route::resource('expenses', 'ExpenseController'); // name = (expenses. index / create / show / update / destroy / edit
Route::resource('expenses', 'ExpenseController'); // name = (expenses. index / create / show / update / destroy / edit
Route::post('expenses/bulk', 'ExpenseController@bulk')->name('expenses.bulk');
Route::post('expenses/bulk', 'ExpenseController@bulk')->name('expenses.bulk');
Route::resource('vendors', 'VendorController'); // name = (vendors. index / create / show / update / destroy / edit
Route::resource('vendors', 'VendorController'); // name = (vendors. index / create / show / update / destroy / edit
Route::post('vendors/bulk', 'VendorController@bulk')->name('vendors.bulk');
Route::post('vendors/bulk', 'VendorController@bulk')->name('vendors.bulk');
Route::resource('client_statement', 'ClientStatementController@statement'); // name = (client_statement. index / create / show / update / destroy / edit
Route::resource('client_statement', 'ClientStatementController@statement'); // name = (client_statement. index / create / show / update / destroy / edit
Route::resource('payments', 'PaymentController'); // name = (payments. index / create / show / update / destroy / edit
Route::resource('payments', 'PaymentController'); // name = (payments. index / create / show / update / destroy / edit
Route::post('payments/refund', 'PaymentController@refund')->name('payments.refund');
Route::post('payments/refund', 'PaymentController@refund')->name('payments.refund');
Route::post('payments/bulk', 'PaymentController@bulk')->name('payments.bulk');
Route::post('payments/bulk', 'PaymentController@bulk')->name('payments.bulk');
Route::post('migrate', 'Migration\MigrateController@index')->name('migrate.start');
// Route::resource('users', 'UserController')->middleware('password_protected'); // name = (users. index / create / show / update / destroy / edit
Route::get('users', 'UserController@index');
Route::put('users/{user}', 'UserController@update')->middleware('password_protected');
Route::post('users', 'UserController@store')->middleware('password_protected');
Route::post('users/{user}/attach_to_company', 'UserController@attach')->middleware('password_protected');
Route::delete('users/{user}/detach_from_company','UserController@detach')->middleware('password_protected');
Route::get('users', 'UserController@index');
Route::put('users/{user}', 'UserController@update')->middleware('password_protected');
Route::post('users', 'UserController@store')->middleware('password_protected');
Route::post('users/{user}/attach_to_company', 'UserController@attach')->middleware('password_protected');
Route::delete('users/{user}/detach_from_company', 'UserController@detach')->middleware('password_protected');
Route::post('users/bulk', 'UserController@bulk')->name('users.bulk')->middleware('password_protected');
Route::post('users/bulk', 'UserController@bulk')->name('users.bulk')->middleware('password_protected');
Route::post('migration/purge/{company}', 'MigrationController@purgeCompany')->middleware('password_protected');
Route::post('migration/purge_save_settings/{company}', 'MigrationController@purgeCompanySaveSettings')->middleware('password_protected');
Route::post('migration/purge/{company}', 'MigrationController@purgeCompany')->middleware('password_protected');
Route::post('migration/purge_save_settings/{company}', 'MigrationController@purgeCompanySaveSettings')->middleware('password_protected');
Route::post('migration/upload_migration', 'MigrationController@uploadMigrationFile')->middleware('password_protected');
Route::resource('companies', 'CompanyController'); // name = (companies. index / create / show / update / destroy / edit
Route::resource('companies', 'CompanyController'); // name = (companies. index / create / show / update / destroy / edit
Route::resource('company_gateways', 'CompanyGatewayController');
Route::resource('company_gateways', 'CompanyGatewayController');
Route::resource('group_settings', 'GroupSettingController');
Route::resource('group_settings', 'GroupSettingController');
Route::resource('tax_rates', 'TaxRateController'); // name = (tasks. index / create / show / update / destroy / edit
Route::resource('tax_rates', 'TaxRateController'); // name = (tasks. index / create / show / update / destroy / edit
Route::post('refresh', 'Auth\LoginController@refresh');
Route::post('refresh', 'Auth\LoginController@refresh');
Route::post('templates', 'TemplateController@show')->name('templates.show');
Route::post('templates', 'TemplateController@show')->name('templates.show');
/*
Route::resource('tasks', 'TaskController'); // name = (tasks. index / create / show / update / destroy / edit
/*
Route::resource('tasks', 'TaskController'); // name = (tasks. index / create / show / update / destroy / edit
Route::post('tasks/bulk', 'TaskController@bulk')->name('tasks.bulk');
Route::post('tasks/bulk', 'TaskController@bulk')->name('tasks.bulk');
Route::resource('credits', 'CreditController'); // name = (credits. index / create / show / update / destroy / edit
Route::resource('credits', 'CreditController'); // name = (credits. index / create / show / update / destroy / edit
Route::post('credits/bulk', 'CreditController@bulk')->name('credits.bulk');
Route::post('credits/bulk', 'CreditController@bulk')->name('credits.bulk');
Route::get('settings', 'SettingsController@index')->name('user.settings');
*/
Route::post('support/messages/send', 'Support\Messages\SendingController');
Route::get('settings', 'SettingsController@index')->name('user.settings');
*/
Route::post('support/messages/send', 'Support\Messages\SendingController');
});
Route::fallback('BaseController@notFound');
Route::fallback('BaseController@notFound');

View File

@ -93,4 +93,22 @@ class MigrationTest extends TestCase
$this->assertNotNull($this->company->settings->timezone_id);
}
public function testMigrationFileUpload()
{
$data = [];
$token = $this->company->tokens->first()->token;
$response = $this->withHeaders([
'X-API-TOKEN' => $token,
'X-API-SECRET' => config('ninja.api_secret'),
'X-Requested-With' => 'XMLHttpRequest'
])->post('/api/v1/migration/upload_migration', $data);
dd($response->getContent()); // "{"message":"Access denied","errors":[]}"
$response->assertStatus(200);
$this->assertTrue(file_exists(base_path('migrations/migration/migration.json')));
}
}

View File

@ -0,0 +1,604 @@
<?php
namespace Tests\Unit\Migration;
use App\Exceptions\MigrationValidatorFailed;
use App\Exceptions\ResourceDependencyMissing;
use App\Exceptions\ResourceNotAvailableForMigration;
use App\Jobs\Util\Import;
use App\Jobs\Util\StartMigration;
use App\Models\Client;
use App\Models\ClientContact;
use App\Models\Company;
use App\Models\Credit;
use App\Models\Invoice;
use App\Models\InvoiceInvitation;
use App\Models\Payment;
use App\Models\Product;
use App\Models\Quote;
use App\Models\TaxRate;
use App\Models\User;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Str;
use Tests\MockAccountData;
use Tests\TestCase;
class ImportTest extends TestCase
{
use MockAccountData;
use DatabaseTransactions;
public function setUp(): void
{
parent::setUp();
$this->makeTestData();
}
public function testImportClassExists()
{
$status = class_exists('App\Jobs\Util\Import');
$this->assertTrue($status);
}
/**
* Ensure exception is thrown when resource
* is not available for the migration.
*/
public function testExceptionOnUnavailableResource()
{
try {
$data['panda_bears'] = [
'name' => 'Awesome Panda Bear',
];
Import::dispatchNow($data, $this->company, $this->user);
} catch (ResourceNotAvailableForMigration $e) {
$this->assertTrue(true);
}
}
public function testCompanyUpdating()
{
$original_company_key = $this->company->company_key;
$data['company'] = [
'company_key' => 0,
];
Import::dispatchNow($data, $this->company, $this->user);
$this->assertNotEquals($original_company_key, $this->company->company_key);
}
public function testTaxRatesInserting()
{
$total_tax_rates = TaxRate::count();
$data['tax_rates'] = [
0 => [
'name' => 'My awesome tax rate 1',
'rate' => '1.000',
]
];
Import::dispatchNow($data, $this->company, $this->user);
$this->assertNotEquals($total_tax_rates, TaxRate::count());
}
public function testTaxRateUniqueValidation()
{
$original_number = TaxRate::count();
try {
$data['tax_rates'] = [
0 => [
'name' => '',
'rate' => '1.000',
],
1 => [
'name' => 'My awesome tax rate 1',
'rate' => '1.000',
]
];
Import::dispatchNow($data, $this->company, $this->user);
} catch (MigrationValidatorFailed $e) {
$this->assertTrue(true);
}
$this->assertEquals($original_number, TaxRate::count());
}
public function testUsersImporting()
{
$original_number = User::count();
$data['users'] = [
0 => [
'id' => 1,
'first_name' => 'David',
'last_name' => 'IN',
'email' => 'my@awesomemail.com',
]
];
Import::dispatchNow($data, $this->company, $this->user);
$this->assertGreaterThan($original_number, User::count());
}
public function testUserValidator()
{
$original_number = User::count();
try {
$data['users'] = [
0 => [
'id' => 1,
'first_name' => 'David',
'last_name' => 'IN',
'email' => 'my@awesomemail.com',
],
1 => [
'id' => 2,
'first_name' => 'Someone',
'last_name' => 'Else',
'email' => 'my@awesomemail.com',
]
];
Import::dispatchNow($data, $this->company, $this->user);
} catch (MigrationValidatorFailed $e) {
$this->assertTrue(true);
}
$this->assertEquals($original_number, User::count());
}
public function testClientImporting()
{
$original_number = Client::count();
$data['users'] = [
0 => [
'id' => 1,
'first_name' => 'David',
'last_name' => 'IN',
'email' => 'my@awesomemail.com',
],
1 => [
'id' => 2,
'first_name' => 'Someone',
'last_name' => 'Else',
'email' => 'my@awesomemail2.com',
]
];
$data['clients'] = [
0 => [
'id' => 1,
'name' => 'My awesome client',
'balance' => '0.00',
'user_id' => 1,
]
];
Import::dispatchNow($data, $this->company, $this->user);
$this->assertGreaterThan($original_number, Client::count());
}
public function testProductsImporting()
{
$original_number = Product::count();
$data['products'] = [
0 => [
"company_id" => 1,
"user_id" => 1,
"custom_value1" => null,
"custom_value2" => null,
"product_key" => "et",
"notes" => "Natus repudiandae occaecati odit est aliquam reiciendis. Nihil sit praesentium excepturi provident nostrum sint. In fugit a dicta voluptas neque quo vel ullam.",
"cost" => "5.0000",
"quantity" => "0.0000",
"tax_name1" => null,
"tax_name2" => null,
"tax_rate1" => "0.000",
"tax_rate2" => "0.000",
"created_at" => "2020-01-22",
"updated_at" => "2020-01-22",
"deleted_at" => null
],
];
Import::dispatchNow($data, $this->company, $this->user);
$this->assertGreaterThan($original_number, Product::count());
}
public function testInvoicesFailsWithoutClient()
{
try {
$data['invoices'] = [
0 => [
'client_id' => 1,
'is_amount_discount' => false,
]
];
Import::dispatchNow($data, $this->company, $this->user);
} catch (ResourceDependencyMissing $e) {
$this->assertTrue(true);
}
}
public function testInvoicesImporting()
{
$original_number = Invoice::count();
$data['clients'] = [
0 => [
'id' => 1,
'name' => 'My awesome client',
'balance' => '0.00',
'user_id' => 1,
]
];
$data['invoices'] = [
0 => [
'id' => 1,
'client_id' => 1,
'discount' => '0.00',
]
];
Import::dispatchNow($data, $this->company, $this->user);
$this->assertGreaterThan($original_number, Invoice::count());
Invoice::where('id', '>=', '0')->forceDelete();
$this->assertEquals(0, Invoice::count());
}
public function testQuotesFailsWithoutClient()
{
try {
$data['quotes'] = [
0 => [
'client_id' => 1,
'is_amount_discount' => false,
]
];
Import::dispatchNow($data, $this->company, $this->user);
} catch (ResourceDependencyMissing $e) {
$this->assertTrue(true);
}
}
public function testQuotesImporting()
{
$original_number = Quote::count();
$data['clients'] = [
0 => [
'id' => 1,
'name' => 'My awesome client',
'balance' => '0.00',
'user_id' => 1,
]
];
$data['quotes'] = [
0 => [
'client_id' => 1,
'discount' => '0.00',
]
];
Import::dispatchNow($data, $this->company, $this->user);
$this->assertGreaterThan($original_number, Quote::count());
}
public function testImportFileExists()
{
$migration_file = base_path() . '/tests/Unit/Migration/migration.json';
$this->assertTrue(file_exists($migration_file));
$migration_array = json_decode(file_get_contents($migration_file), 1);
$this->assertGreaterThan(1, count($migration_array));
}
public function testAllImport()
{
//$this->makeTestData();
$this->invoice->forceDelete();
$migration_file = base_path() . '/tests/Unit/Migration/migration.json';
$migration_array = json_decode(file_get_contents($migration_file), 1);
Import::dispatchNow($migration_array, $this->company, $this->user);
$this->assertTrue(true);
}
public function testClientAttributes()
{
$original_number = Client::count();
$random_balance = rand(0, 10);
$data['clients'] = [
0 => [
'id' => 1,
'name' => 'My awesome unique client',
'balance' => $random_balance,
'user_id' => 1,
]
];
Import::dispatchNow($data, $this->company, $this->user);
$client = Client::where('name', 'My awesome unique client')
->where('balance', $random_balance)
->first();
// Originally was checked with ClientContact::whereEmail() but it throws 'array to string conversion' on insert.
$this->assertNotNull($client);
$this->assertGreaterThan($original_number, Client::count());
$this->assertGreaterThanOrEqual(0, $client->balance);
}
public function testInvoiceImporting()
{
$original_number = Invoice::count();
$this->invoice->forceDelete();
$migration_file = base_path() . '/tests/Unit/Migration/migration.json';
$migration_array = json_decode(file_get_contents($migration_file), 1);
Import::dispatchNow($migration_array, $this->company, $this->user);
$this->assertGreaterThan($original_number, Invoice::count());
}
public function testInvoiceAttributes()
{
$original_number = Invoice::count();
$this->invoice->forceDelete();
$migration_file = base_path() . '/tests/Unit/Migration/migration.json';
$migration_array = json_decode(file_get_contents($migration_file), 1);
Import::dispatchNow($migration_array, $this->company, $this->user);
$this->assertGreaterThan($original_number, Invoice::count());
$invoice_1 = Invoice::where('number', '0001')
->where('discount', '0.00')
->where('date', '2020-03-18')
->first();
$invoice_2 = Invoice::where('number', '0018')
->where('discount', '0.00')
->where('date', '2019-10-15')
->first();
$this->assertNotNull($invoice_1);
$this->assertNotNull($invoice_2);
$this->assertEquals('43.7500', $invoice_1->amount);
$this->assertEquals('55.2600', $invoice_2->amount);
$this->assertEquals('18.7700', $invoice_1->balance);
$this->assertEquals('49.3700', $invoice_2->balance);
}
public function testQuoteAttributes()
{
$original_number = Quote::count();
$this->invoice->forceDelete();
$migration_file = base_path() . '/tests/Unit/Migration/migration.json';
$migration_array = json_decode(file_get_contents($migration_file), 1);
Import::dispatchNow($migration_array, $this->company, $this->user);
$this->assertGreaterThan($original_number, Invoice::count());
$quote = Quote::where('number', '0002')
->where('discount', '0.00')
->where('date', '2020-04-26')
->first();
$this->assertNotNull($quote);
$this->assertEquals('0.0000', $quote->amount);
$this->assertEquals('0.0000', $quote->balance);
}
public function testPaymentsImport()
{
$original_count = Payment::count();
$this->invoice->forceDelete();
$migration_file = base_path() . '/tests/Unit/Migration/migration.json';
$migration_array = json_decode(file_get_contents($migration_file), 1);
Import::dispatchNow($migration_array, $this->company, $this->user);
$this->assertGreaterThan($original_count, Payment::count());
}
public function testPaymentDependsOnClient()
{
try {
$data['payments'] = [
0 => [
'client_id' => 1,
'amount' => 1,
]
];
Import::dispatchNow($data, $this->company, $this->user);
} catch (ResourceDependencyMissing $e) {
$this->assertTrue(true);
}
}
public function testQuotesImport()
{
$original_count = Credit::count();
$this->invoice->forceDelete();
$migration_file = base_path() . '/tests/Unit/Migration/migration.json';
$migration_array = json_decode(file_get_contents($migration_file), 1);
Import::dispatchNow($migration_array, $this->company, $this->user);
$this->assertGreaterThan($original_count, Credit::count());
}
public function testMigrationFileExists()
{
$migration_archive = base_path() . '/tests/Unit/Migration/migration.zip';
$this->assertTrue(file_exists($migration_archive));
}
public function testMigrationFileBeingExtracted()
{
$migration_archive = base_path() . '/tests/Unit/Migration/migration.zip';
StartMigration::dispatchNow($migration_archive, $this->user, $this->company);
$extracted_archive = storage_path("migrations/migration");
$migration_file = storage_path("migrations/migration/migration.json");
$this->assertTrue(file_exists($extracted_archive));
$this->assertTrue(is_dir($extracted_archive));
$this->assertTrue(file_exists($migration_file));
}
public function testValidityOfImportedData()
{
$this->invoice->forceDelete();
$migration_file = base_path() . '/tests/Unit/Migration/migration.json';
$migration_array = json_decode(file_get_contents($migration_file), 1);
Import::dispatchNow($migration_array, $this->company, $this->user);
$differences = [];
foreach ($migration_array['users'] as $key => $user) {
$record = User::where('email', $user['email'])->first();
if (!$record) {
$differences['users']['missing'][] = $user['email'];
}
}
foreach ($migration_array['tax_rates'] as $key => $tax_rate) {
$record = TaxRate::where('name', $tax_rate['name'])
->where('rate', $tax_rate['rate'])
->first();
if (!$record) {
$differences['tax_rates']['missing'][] = $tax_rate['name'];
}
}
foreach ($migration_array['clients'] as $key => $client) {
$record = Client::where('name', $client['name'])
->where('city', $client['city'])
->first();
if (!$record) {
$differences['clients']['missing'][] = $client['name'];
}
}
/* foreach ($migration_array['products'] as $key => $product) {
$record = Product::where('product_key', $product['product_key'])
->where('quantity', $product['quantity'])
->first();
if (!$record) {
$differences['products']['missing'][] = $product['notes'];
}
} */
foreach ($migration_array['invoices'] as $key => $invoices) {
$record = Invoice::where('number', $invoices['number'])
->where('is_amount_discount', $invoices['is_amount_discount'])
->where('due_date', $invoices['due_date'])
->first();
if (!$record) {
$differences['invoices']['missing'][] = $invoices['id'];
}
}
foreach ($migration_array['quotes'] as $key => $quote) {
$record = Quote::where('number', $quote['number'])
->where('is_amount_discount', $quote['is_amount_discount'])
->where('due_date', $quote['due_date'])
->first();
if (!$record) {
$differences['quotes']['missing'][] = $quote['id'];
}
}
foreach ($migration_array['payments'] as $key => $payment) {
$record = Payment::where('amount', $payment['amount'])
->where('applied', $payment['applied'])
->where('refunded', $payment['refunded'])
->first();
if (!$record) {
$differences['quotes']['missing'][] = $payment['id'];
}
}
/*foreach ($migration_array['credits'] as $key => $credit) {
$record = Credit::where('number', $credit['number'])
->where('date', $credit['date'])
->first();
if (!$record) {
$differences['credits']['missing'][] = $credit['id'];
}
}*/
$this->assertCount(0, $differences);
}
}

File diff suppressed because one or more lines are too long

Binary file not shown.