diff --git a/.env.example b/.env.example index 69001f07b224..95ce31237f59 100644 --- a/.env.example +++ b/.env.example @@ -1,6 +1,6 @@ APP_NAME="Invoice Ninja" APP_ENV=production -APP_KEY= +APP_KEY=base64:RR++yx2rJ9kdxbdh3+AmbHLDQu+Q76i++co9Y8ybbno= APP_DEBUG=false APP_URL=http://localhost diff --git a/VERSION.txt b/VERSION.txt index 045285d57c8a..f3a1d04bd364 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -1 +1 @@ -5.3.54 \ No newline at end of file +5.3.55 \ No newline at end of file diff --git a/app/Http/Controllers/ClientController.php b/app/Http/Controllers/ClientController.php index a867b9045b77..f3f8155851aa 100644 --- a/app/Http/Controllers/ClientController.php +++ b/app/Http/Controllers/ClientController.php @@ -19,6 +19,7 @@ use App\Http\Requests\Client\AdjustClientLedgerRequest; use App\Http\Requests\Client\CreateClientRequest; use App\Http\Requests\Client\DestroyClientRequest; use App\Http\Requests\Client\EditClientRequest; +use App\Http\Requests\Client\PurgeClientRequest; use App\Http\Requests\Client\ShowClientRequest; use App\Http\Requests\Client\StoreClientRequest; use App\Http\Requests\Client\UpdateClientRequest; @@ -36,7 +37,7 @@ use App\Utils\Traits\SavesDocuments; use App\Utils\Traits\Uploadable; use Illuminate\Http\Request; use Illuminate\Http\Response; - +use Illuminate\Support\Facades\Storage; /** * Class ClientController. * @covers App\Http\Controllers\ClientController @@ -510,7 +511,7 @@ class ClientController extends BaseController $ids = request()->input('ids'); $clients = Client::withTrashed()->whereIn('id', $this->transformKeys($ids))->cursor(); - if(!in_array($action, ['restore','archive','delete','purge'])) + if(!in_array($action, ['restore','archive','delete'])) return response()->json(['message' => 'That action is not available.'], 400); $clients->each(function ($client, $key) use ($action) { @@ -586,5 +587,71 @@ class ClientController extends BaseController } + /** + * Update the specified resource in storage. + * + * @param UploadClientRequest $request + * @param Client $client + * @return Response + * + * + * + * @OA\Put( + * path="/api/v1/clients/{id}/purge", + * operationId="uploadClient", + * tags={"clients"}, + * summary="Purges a client from the system", + * description="Handles purging a client", + * @OA\Parameter(ref="#/components/parameters/X-Api-Secret"), + * @OA\Parameter(ref="#/components/parameters/X-Api-Token"), + * @OA\Parameter(ref="#/components/parameters/X-Requested-With"), + * @OA\Parameter(ref="#/components/parameters/include"), + * @OA\Parameter( + * name="id", + * in="path", + * description="The Client Hashed ID", + * example="D2J234DFA", + * required=true, + * @OA\Schema( + * type="string", + * format="string", + * ), + * ), + * @OA\Response( + * response=200, + * description="Returns the client object", + * @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 purge(PurgeClientRequest $request, Client $client) + { + //delete all documents + $client->documents->each(function ($document){ + + Storage::disk(config('filesystems.default'))->delete($document->url); + + }); + + //force delete the client + $this->client_repo->purge($client); + + return response()->json(['message' => 'Success'], 200); + + //todo add an event here using the client name as reference for purge event + } } diff --git a/app/Http/Controllers/ClientPortal/SubscriptionPurchaseController.php b/app/Http/Controllers/ClientPortal/SubscriptionPurchaseController.php index bc61177fb42a..735b6fac7e7c 100644 --- a/app/Http/Controllers/ClientPortal/SubscriptionPurchaseController.php +++ b/app/Http/Controllers/ClientPortal/SubscriptionPurchaseController.php @@ -24,6 +24,12 @@ class SubscriptionPurchaseController extends Controller { public function index(Subscription $subscription, Request $request) { + /* Make sure the contact is logged into the correct company for this subscription */ + if(auth()->guard('contact')->user() && auth()->guard('contact')->user()->company_id != $subscription->company_id){ + auth()->guard('contact')->logout(); + $request->session()->invalidate(); + } + if ($request->has('locale')) { $this->setLocale($request->query('locale')); } diff --git a/app/Http/Requests/Client/PurgeClientRequest.php b/app/Http/Requests/Client/PurgeClientRequest.php new file mode 100644 index 000000000000..7b392c89cb94 --- /dev/null +++ b/app/Http/Requests/Client/PurgeClientRequest.php @@ -0,0 +1,27 @@ +user()->isAdmin(); + } +} diff --git a/app/Http/Requests/Request.php b/app/Http/Requests/Request.php index a53cd70540c6..628fb76ef547 100644 --- a/app/Http/Requests/Request.php +++ b/app/Http/Requests/Request.php @@ -39,6 +39,10 @@ class Request extends FormRequest } } + //01-02-2022 needed for CSV Imports + if(!$merge_rules) + return $rules; + return array_merge($merge_rules, $rules); } diff --git a/app/Http/ValidationRules/Company/ValidCompanyQuantity.php b/app/Http/ValidationRules/Company/ValidCompanyQuantity.php index 1e5de928edde..6dd88aab32d3 100644 --- a/app/Http/ValidationRules/Company/ValidCompanyQuantity.php +++ b/app/Http/ValidationRules/Company/ValidCompanyQuantity.php @@ -11,6 +11,7 @@ namespace App\Http\ValidationRules\Company; +use App\Utils\Ninja; use Illuminate\Contracts\Validation\Rule; /** @@ -25,7 +26,12 @@ class ValidCompanyQuantity implements Rule */ public function passes($attribute, $value) { - return auth()->user()->company()->account->companies->count() <= 10; + if(Ninja::isSelfHost()) + return auth()->user()->company()->account->companies->count() < 10; + + + return auth()->user()->company()->account->companies->count() < auth()->user()->company()->account->hosted_company_count; + } /** diff --git a/app/Http/ValidationRules/Ninja/CanStoreClientsRule.php b/app/Http/ValidationRules/Ninja/CanStoreClientsRule.php index a1523c2f2efd..23221c042c2b 100644 --- a/app/Http/ValidationRules/Ninja/CanStoreClientsRule.php +++ b/app/Http/ValidationRules/Ninja/CanStoreClientsRule.php @@ -35,7 +35,7 @@ class CanStoreClientsRule implements Rule { $company = Company::find($this->company_id); - return $company->clients->count() < config('ninja.quotas.free.clients'); + return $company->clients->count() < $company->account->hosted_client_count; } /** @@ -43,6 +43,6 @@ class CanStoreClientsRule implements Rule */ public function message() { - return ctrans('texts.limit_clients', ['count' => config('ninja.quotas.free.clients')]); + return ctrans('texts.limit_clients', ['count' => $company->account->hosted_client_count]); } } diff --git a/app/Import/Providers/BaseImport.php b/app/Import/Providers/BaseImport.php new file mode 100644 index 000000000000..995c759fff64 --- /dev/null +++ b/app/Import/Providers/BaseImport.php @@ -0,0 +1,306 @@ +company = $company; + $this->request = $request; + $this->hash = $request['hash']; + $this->import_type = $request['import_type']; + $this->skip_header = $request['skip_header'] ?? null; + $this->column_map = + ! empty( $request['column_map'] ) ? + array_combine( array_keys( $request['column_map'] ), array_column( $request['column_map'], 'mapping' ) ) : null; + + auth()->login( $this->company->owner(), true ); + + auth()->user()->setCompany($this->company); + } + + + protected function getCsvData( $entity_type ) { + + $base64_encoded_csv = Cache::pull( $this->hash . '-' . $entity_type ); + if ( empty( $base64_encoded_csv ) ) { + return null; + } + + $csv = base64_decode( $base64_encoded_csv ); + $csv = Reader::createFromString( $csv ); + + $stmt = new Statement(); + $data = iterator_to_array( $stmt->process( $csv ) ); + + if ( count( $data ) > 0 ) { + $headers = $data[0]; + + // Remove Invoice Ninja headers + if ( count( $headers ) && count( $data ) > 4 && $this->import_type === 'csv' ) { + $first_cell = $headers[0]; + if ( strstr( $first_cell, config( 'ninja.app_name' ) ) ) { + array_shift( $data ); // Invoice Ninja... + array_shift( $data ); // + array_shift( $data ); // Enitty Type Header + } + } + } + + return $data; + } + + public function mapCSVHeaderToKeys( $csvData ) { + $keys = array_shift( $csvData ); + + return array_map( function ( $values ) use ( $keys ) { + return array_combine( $keys, $values ); + }, $csvData ); + } + + private function groupInvoices( $csvData, $key ) { + // Group by invoice. + $grouped = []; + + foreach ( $csvData as $line_item ) { + if ( empty( $line_item[ $key ] ) ) { + $this->error_array['invoice'][] = [ 'invoice' => $line_item, 'error' => 'No invoice number' ]; + } else { + $grouped[ $line_item[ $key ] ][] = $line_item; + } + } + + return $grouped; + } + + public function getErrors() + { + return $this->error_array; + } + + public function ingest($data, $entity_type) + { + $count = 0; + + foreach ( $data as $record ) { + try { + $entity = $this->transformer->transform( $record ); + + /** @var \App\Http\Requests\Request $request */ + $request = new $this->request_name(); + + // Pass entity data to request so it can be validated + $request->query = $request->request = new ParameterBag( $entity ); + $validator = Validator::make( $entity, $request->rules() ); + + if ( $validator->fails() ) { + $this->error_array[ $entity_type ][] = + [ $entity_type => $record, 'error' => $validator->errors()->all() ]; + } else { + $entity = + $this->repository->save( + array_diff_key( $entity, [ 'user_id' => false ] ), + $this->factory_name::create( $this->company->id, $this->getUserIDForRecord( $entity ) ) ); + + $entity->saveQuietly(); + $count++; + + } + } catch ( \Exception $ex ) { + if ( $ex instanceof ImportException ) { + $message = $ex->getMessage(); + } else { + report( $ex ); + $message = 'Unknown error'; + } + + $this->error_array[ $entity_type ][] = [ $entity_type => $record, 'error' => $message ]; + } + + return $count; + } + } + + public function ingestInvoices( $invoices ) { + $invoice_transformer = $this->transformer; + + /** @var PaymentRepository $payment_repository */ + $payment_repository = app()->make( PaymentRepository::class ); + $payment_repository->import_mode = true; + + /** @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 ) { + try { + $invoice_data = $invoice_transformer->transform( $raw_invoice ); + + $invoice_data['line_items'] = $this->cleanItems( $invoice_data['line_items'] ?? [] ); + + + // If we don't have a client ID, but we do have client data, go ahead and create the client. + 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 = Validator::make( $invoice_data, ( new StoreInvoiceRequest() )->rules() ); + if ( $validator->fails() ) { + $this->error_array['invoice'][] = + [ 'invoice' => $invoice_data, 'error' => $validator->errors()->all() ]; + } else { + $invoice = InvoiceFactory::create( $this->company->id, $this->getUserIDForRecord( $invoice_data ) ); + if ( ! empty( $invoice_data['status_id'] ) ) { + $invoice->status_id = $invoice_data['status_id']; + } + $invoice_repository->save( $invoice_data, $invoice ); + $this->addInvoiceToMaps( $invoice ); + + // If we're doing a generic CSV import, only import payment data if we're not importing a payment CSV. + // If we're doing a platform-specific import, trust the platform to only return payment info if there's not a separate payment CSV. + if ( $this->import_type !== 'csv' || empty( $this->column_map['payment'] ) ) { + // Check for payment columns + if ( ! empty( $invoice_data['payments'] ) ) { + foreach ( $invoice_data['payments'] as $payment_data ) { + $payment_data['user_id'] = $invoice->user_id; + $payment_data['client_id'] = $invoice->client_id; + $payment_data['invoices'] = [ + [ + 'invoice_id' => $invoice->id, + 'amount' => $payment_data['amount'] ?? null, + ], + ]; + + /* Make sure we don't apply any payments to invoices with a Zero Amount*/ + if($invoice->amount > 0) + { + $payment_repository->save( + $payment_data, + PaymentFactory::create( $this->company->id, $invoice->user_id, $invoice->client_id ) + ); + } + } + } + } + + $this->actionInvoiceStatus( $invoice, $invoice_data, $invoice_repository ); + } + } catch ( \Exception $ex ) { + if ( $ex instanceof ImportException ) { + $message = $ex->getMessage(); + } else { + report( $ex ); + $message = 'Unknown error'; + } + + $this->error_array['invoice'][] = [ 'invoice' => $raw_invoice, 'error' => $message ]; + } + } + } + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + protected function getUserIDForRecord( $record ) { + if ( ! empty( $record['user_id'] ) ) { + return $this->findUser( $record['user_id'] ); + } else { + return $this->company->owner()->id; + } + } + + protected function findUser( $user_hash ) { + $user = User::where( 'account_id', $this->company->account->id ) + ->where( \DB::raw( 'CONCAT_WS(" ", first_name, last_name)' ), 'like', '%' . $user_hash . '%' ) + ->first(); + + if ( $user ) { + return $user->id; + } else { + return $this->company->owner()->id; + } + } + +} diff --git a/app/Import/Providers/Csv.php b/app/Import/Providers/Csv.php new file mode 100644 index 000000000000..0eee3c42a85c --- /dev/null +++ b/app/Import/Providers/Csv.php @@ -0,0 +1,166 @@ +{$entity}(); + + //collate any errors + } + + private function client() + { + + $entity_type = 'client'; + + $data = $this->getCsvData($entity_type); + + $data = $this->preTransform($data, $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; + + } + + private function product() + { + + $entity_type = 'product'; + + $data = $this->getCsvData($entity_type); + + $data = $this->preTransform($data, $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); + + $product_count = $this->ingest($data, $entity_type); + + $this->entity_count['products'] = $product_count; + + } + + private function invoice() + { + + $entity_type = 'invoice'; + + $data = $this->getCsvData($entity_type); + + $data = $this->preTransform($data, $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 ProductTransformer($this->company); + + $invoice_count = $this->ingest($data, $entity_type); + + $this->entity_count['invoices'] = $invoice_count; + + } + + + public function preTransform(array $data, $entity_type) + { + + + if ( empty( $this->column_map[ $entity_type ] ) ) { + return false; + } + + if ( $this->skip_header ) { + array_shift( $data ); + } + + //sort the array by key + $keys = $this->column_map[ $entity_type ]; + ksort( $keys ); + + $data = array_map( function ( $row ) use ( $keys ) { + return array_combine( $keys, array_intersect_key( $row, $keys ) ); + }, $data ); + + + return $data; + + + } + + public function transform(array $data) + { + + } + +} \ No newline at end of file diff --git a/app/Import/Providers/Freshbooks.php b/app/Import/Providers/Freshbooks.php new file mode 100644 index 000000000000..3f25795848f4 --- /dev/null +++ b/app/Import/Providers/Freshbooks.php @@ -0,0 +1,16 @@ +company = $company; + } + + public function getString($data, $field) + { + return (isset($data[$field]) && $data[$field]) ? $data[$field] : ''; + } + + public function getCurrencyByCode( $data, $key = 'client.currency_id' ) + { + + $code = array_key_exists( $key, $data ) ? $data[ $key ] : false; + + $currencies = Cache::get('currencies'); + + $currency = $currencies->filter(function ($item) use($code) { + return $item->code == $code; + })->first(); + + return $currency ? $currency->id : $this->company->settings->currency_id; + + } + + public function getClient($client_name, $client_email) { + + $client_id_search = $this->company->clients()->where( 'id_number', $client_name ); + + if ( $client_id_search->count() >= 1 ) { + return $client_id_search->first()->id; + nlog("found via id number"); + } + + $client_name_search = $this->company->clients()->where( 'name', $client_name ); + + if ( $client_name_search->count() >= 1 ) { + return $client_name_search->first()->id; + nlog("found via name"); + } + + if ( ! empty( $client_email ) ) { + $contacts = ClientContact::where( 'company_id', $this->company->id ) + ->where( 'email', $client_email ); + + if ( $contacts->count() >= 1 ) { + return $contacts->first()->client_id; + nlog("found via contact"); + } + } + nlog("did not find client"); + + return null; + } + + + + /////////////////////////////////////////////////////////////////////////////////// + /** + * @param $name + * + * @return bool + */ + public function hasClient($name) + { + return $this->company->clients()->whereRaw("LOWER(REPLACE(`name`, ' ' ,'')) = ?", [strtolower(str_replace(' ', '', $name))])->exists(); + } + + /** + * @param $name + * + * @return bool + */ + public function hasVendor($name) + { + return $this->company->vendors()->whereRaw("LOWER(REPLACE(`name`, ' ' ,'')) = ?", [strtolower(str_replace(' ', '', $name))])->exists(); + } + + /** + * @param $key + * + * @return bool + */ + public function hasProduct($key) + { + return $this->company->products()->whereRaw("LOWER(REPLACE(`product_key`, ' ' ,'')) = ?", [strtolower(str_replace(' ', '', $key))])->exists(); + } + + /** + * @param $data + * @param $field + * + * @return float + */ + public function getFloat($data, $field) + { + if (array_key_exists($field, $data)) { + $number = preg_replace('/[^0-9-.]+/', '', $data[$field]); + } else { + $number = 0; + } + + return Number::parseFloat($number); + } + + /** + * @param $name + * + * @return int|null + */ + public function getClientId($name) + { + $client = $this->company->clients()->whereRaw("LOWER(REPLACE(`name`, ' ' ,'')) = ?", [strtolower(str_replace(' ', '', $name))])->first(); + + return $client ? $client->id : null; + } + + /** + * @param $name + * + * @return string + */ + public function getProduct($data, $key, $field, $default = false) + { + + $product = $this->company->products()->whereRaw("LOWER(REPLACE(`product_key`, ' ' ,'')) = ?", [strtolower(str_replace(' ', '', $data->{$key}))])->first(); + + if($product) + return $product->{$field} ?: $default; + + return $default; + + } + + /** + * @param $email + * + * @return ?Contact + */ + public function getContact($email) + { + + $contact = $this->company->client_contacts()->whereRaw("LOWER(REPLACE(`email`, ' ' ,'')) = ?", [strtolower(str_replace(' ', '', $email))])->first(); + + if(!$contact) + return null; + + return $contact; + + } + + /** + * @param $name + * + * @return int|null + */ + public function getCountryId($name) + { + if(strlen($name) == 2) + return $this->getCountryIdBy2($name); + + $country = Country::whereRaw("LOWER(REPLACE(`name`, ' ' ,'')) = ?", [strtolower(str_replace(' ', '', $name))])->first(); + + return $country ? $country->id : null; + } + + /** + * @param $name + * + * @return int|null + */ + public function getCountryIdBy2($name) + { + return Country::where('iso_3166_2', $name)->exists() ? Country::where('iso_3166_2', $name)->first()->id : null; + } + + /** + * @param $name + * + * @return int + */ + public function getTaxRate($name) + { + $name = strtolower(trim($name)); + + $tax_rate = $this->company->tax_rates()->whereRaw("LOWER(REPLACE(`name`, ' ' ,'')) = ?", [strtolower(str_replace(' ', '', $name))])->first(); + + return $tax_rate ? $tax_rate->rate : 0; + } + + /** + * @param $name + * + * @return string + */ + public function getTaxName($name) + { + $name = strtolower(trim($name)); + + $tax_rate = $this->company->tax_rates()->whereRaw("LOWER(REPLACE(`name`, ' ' ,'')) = ?", [strtolower(str_replace(' ', '', $name))])->first(); + + return $tax_rate ? $tax_rate->name : ''; + + } + + /** + * @param $date + * @param string $format + * @param mixed $data + * @param mixed $field + * + * @return null + */ + public function getDate($data, $field) + { + if ($date = data_get($data, $field)) { + try { + $date = new Carbon($date); + } catch (\Exception $e) { + // if we fail to parse return blank + $date = false; + } + } + + return $date ? $date->format('Y-m-d') : null; + } + + /** + * @param $number + * + * @return ?string + */ + public function getInvoiceNumber($number) + { + return $number ? ltrim( trim( $number ), '0' ) : null; + } + + /** + * @param $invoice_number + * + * @return int|null + */ + public function getInvoiceId($invoice_number) + { + $invoice = $this->company->invoices()->whereRaw("LOWER(REPLACE(`number`, ' ' ,'')) = ?", [strtolower(str_replace(' ', '', $invoice_number))])->first(); + + return $invoice ? $invoice->id : null; + } + + /** + * @param $invoice_number + * + * @return bool + */ + public function hasInvoice($invoice_number) + { + + return $this->company->invoices()->whereRaw("LOWER(REPLACE(`number`, ' ' ,'')) = ?", [strtolower(str_replace(' ', '', $invoice_number))])->exists(); + + } + + /** + * @param $invoice_number + * + * @return int|null + */ + public function getInvoiceClientId($invoice_number) + { + $invoice = $this->company->invoices()->whereRaw("LOWER(REPLACE(`number`, ' ' ,'')) = ?", [strtolower(str_replace(' ', '', $invoice_number))])->first(); + + return $invoice ? $invoice->client_id : null; + } + + /** + * @param $name + * + * @return int|null + */ + public function getVendorId($name) + { + $vendor = $this->company->vendors()->whereRaw("LOWER(REPLACE(`name`, ' ' ,'')) = ?", [strtolower(str_replace(' ', '', $name))])->first(); + + return $vendor ? $vendor->id : null; + } + + /** + * @param $name + * + * @return int|null + */ + public function getExpenseCategoryId( $name ) { + + $ec = $this->company->expense_categories()->whereRaw("LOWER(REPLACE(`name`, ' ' ,'')) = ?", [strtolower(str_replace(' ', '', $name))])->first(); + + return $ec ? $ec->id : null; + + } + + /** + * @param $name + * + * @return int|null + */ + public function getProjectId( $name ) { + + $project = $this->company->projects()->whereRaw("LOWER(REPLACE(`name`, ' ' ,'')) = ?", [strtolower(str_replace(' ', '', $name))])->first(); + + return $project ? $project->id : null; + } + + /** + * @param $name + * + * @return int|null + */ + public function getPaymentTypeId( $name ) { + + $pt = PaymentType::whereRaw("LOWER(REPLACE(`name`, ' ' ,'')) = ?", [strtolower(str_replace(' ', '', $name))])->first(); + + return $pt ? $pt->id : null; + } + + +} diff --git a/app/Import/Transformer/Csv/ClientTransformer.php b/app/Import/Transformer/Csv/ClientTransformer.php new file mode 100644 index 000000000000..4df6591ff63d --- /dev/null +++ b/app/Import/Transformer/Csv/ClientTransformer.php @@ -0,0 +1,82 @@ +name) && $this->hasClient($data->name)) { + throw new ImportException('Client already exists'); + } + + $settings = new \stdClass; + $settings->currency_id = (string)$this->getCurrencyByCode($data); + + return [ + 'company_id' => $this->company->id, + 'name' => $this->getString( $data, 'client.name' ), + 'work_phone' => $this->getString( $data, 'client.phone' ), + 'address1' => $this->getString( $data, 'client.address1' ), + 'address2' => $this->getString( $data, 'client.address2' ), + 'postal_code' => $this->getString( $data, 'client.postal_code'), + 'city' => $this->getString( $data, 'client.city' ), + 'state' => $this->getString( $data, 'client.state' ), + 'shipping_address1' => $this->getString( $data, 'client.shipping_address1' ), + 'shipping_address2' => $this->getString( $data, 'client.shipping_address2' ), + 'shipping_city' => $this->getString( $data, 'client.shipping_city' ), + 'shipping_state' => $this->getString( $data, 'client.shipping_state' ), + 'shipping_postal_code' => $this->getString( $data, 'client.shipping_postal_code' ), + 'public_notes' => $this->getString( $data, 'client.public_notes' ), + 'private_notes' => $this->getString( $data, 'client.private_notes' ), + 'website' => $this->getString( $data, 'client.website' ), + 'vat_number' => $this->getString( $data, 'client.vat_number' ), + 'id_number' => $this->getString( $data, 'client.id_number' ), + 'custom_value1' => $this->getString( $data, 'client.custom_value1' ), + 'custom_value2' => $this->getString( $data, 'client.custom_value2' ), + 'custom_value3' => $this->getString( $data, 'client.custom_value3' ), + 'custom_value4' => $this->getString( $data, 'client.custom_value4' ), + 'balance' => preg_replace( '/[^0-9,.]+/', '', $this->getFloat( $data, 'client.balance' ) ), + 'paid_to_date' => preg_replace( '/[^0-9,.]+/', '', $this->getFloat( $data, 'client.paid_to_date' ) ), + 'credit_balance' => 0, + 'settings' => $settings, + 'client_hash' => Str::random( 40 ), + 'contacts' => [ + [ + 'first_name' => $this->getString( $data, 'contact.first_name' ), + 'last_name' => $this->getString( $data, 'contact.last_name' ), + 'email' => $this->getString( $data, 'contact.email' ), + 'phone' => $this->getString( $data, 'contact.phone' ), + 'custom_value1' => $this->getString( $data, 'contact.custom_value1' ), + 'custom_value2' => $this->getString( $data, 'contact.custom_value2' ), + 'custom_value3' => $this->getString( $data, 'contact.custom_value3' ), + 'custom_value4' => $this->getString( $data, 'contact.custom_value4' ), + ], + ], + 'country_id' => isset( $data['client.country'] ) ? $this->getCountryId( $data['client.country']) : null, + 'shipping_country_id' => isset($data['client.shipping_country'] ) ? $this->getCountryId( $data['client.shipping_country'] ) : null, + ]; + } +} + + diff --git a/app/Import/Transformer/Csv/InvoiceTransformer.php b/app/Import/Transformer/Csv/InvoiceTransformer.php new file mode 100644 index 000000000000..d182e4cadb0a --- /dev/null +++ b/app/Import/Transformer/Csv/InvoiceTransformer.php @@ -0,0 +1,129 @@ +hasInvoice( $invoice_data['invoice.number'] ) ) { + throw new ImportException( 'Invoice number already exists' ); + } + + $invoiceStatusMap = [ + 'sent' => Invoice::STATUS_SENT, + 'draft' => Invoice::STATUS_DRAFT, + ]; + + $transformed = [ + 'company_id' => $this->company->id, + 'number' => $this->getString( $invoice_data, 'invoice.number' ), + 'user_id' => $this->getString( $invoice_data, 'invoice.user_id' ), + 'amount' => $amount = $this->getFloat( $invoice_data, 'invoice.amount' ), + 'balance' => isset( $invoice_data['invoice.balance'] ) ? $this->getFloat( $invoice_data, 'invoice.balance' ) : $amount, + 'client_id' => $this->getClient( $this->getString( $invoice_data, 'client.name' ), $this->getString( $invoice_data, 'client.email' ) ), + 'discount' => $this->getFloat( $invoice_data, 'invoice.discount' ), + 'po_number' => $this->getString( $invoice_data, 'invoice.po_number' ), + 'date' => isset( $invoice_data['invoice.date'] ) ? date( 'Y-m-d', strtotime( $invoice_data['invoice.date'] ) ) : now()->format('Y-m-d'), + 'due_date' => isset( $invoice_data['invoice.due_date'] ) ? date( 'Y-m-d', strtotime( $invoice_data['invoice.due_date'] ) ) : null, + 'terms' => $this->getString( $invoice_data, 'invoice.terms' ), + 'public_notes' => $this->getString( $invoice_data, 'invoice.public_notes' ), + 'private_notes' => $this->getString( $invoice_data, 'invoice.private_notes' ), + 'tax_name1' => $this->getString( $invoice_data, 'invoice.tax_name1' ), + 'tax_rate1' => $this->getFloat( $invoice_data, 'invoice.tax_rate1' ), + 'tax_name2' => $this->getString( $invoice_data, 'invoice.tax_name2' ), + 'tax_rate2' => $this->getFloat( $invoice_data, 'invoice.tax_rate2' ), + 'tax_name3' => $this->getString( $invoice_data, 'invoice.tax_name3' ), + 'tax_rate3' => $this->getFloat( $invoice_data, 'invoice.tax_rate3' ), + 'custom_value1' => $this->getString( $invoice_data, 'invoice.custom_value1' ), + 'custom_value2' => $this->getString( $invoice_data, 'invoice.custom_value2' ), + 'custom_value3' => $this->getString( $invoice_data, 'invoice.custom_value3' ), + 'custom_value4' => $this->getString( $invoice_data, 'invoice.custom_value4' ), + 'footer' => $this->getString( $invoice_data, 'invoice.footer' ), + 'partial' => $this->getFloat( $invoice_data, 'invoice.partial' ), + 'partial_due_date' => $this->getString( $invoice_data, 'invoice.partial_due_date' ), + 'custom_surcharge1' => $this->getString( $invoice_data, 'invoice.custom_surcharge1' ), + 'custom_surcharge2' => $this->getString( $invoice_data, 'invoice.custom_surcharge2' ), + 'custom_surcharge3' => $this->getString( $invoice_data, 'invoice.custom_surcharge3' ), + 'custom_surcharge4' => $this->getString( $invoice_data, 'invoice.custom_surcharge4' ), + 'exchange_rate' => $this->getString( $invoice_data, 'invoice.exchange_rate' ), + 'status_id' => $invoiceStatusMap[ $status = + strtolower( $this->getString( $invoice_data, 'invoice.status' ) ) ] ?? + Invoice::STATUS_SENT, + 'archived' => $status === 'archived', + ]; + + if ( isset( $invoice_data['payment.amount'] ) ) { + $transformed['payments'] = [ + [ + 'date' => isset( $invoice_data['payment.date'] ) ? date( 'Y-m-d', strtotime( $invoice_data['payment.date'] ) ) : date( 'y-m-d' ), + 'transaction_reference' => $this->getString( $invoice_data, 'payment.transaction_reference' ), + 'amount' => $this->getFloat( $invoice_data, 'payment.amount' ), + ], + ]; + } elseif ( $status === 'paid' ) { + $transformed['payments'] = [ + [ + 'date' => isset( $invoice_data['payment.date'] ) ? date( 'Y-m-d', strtotime( $invoice_data['payment.date'] ) ) : date( 'y-m-d' ), + 'transaction_reference' => $this->getString( $invoice_data, 'payment.transaction_reference' ), + 'amount' => $this->getFloat( $invoice_data, 'invoice.amount' ), + ], + ]; + } elseif ( isset( $transformed['amount'] ) && isset( $transformed['balance'] ) && ($transformed['amount'] != $transformed['balance'])) { + $transformed['payments'] = [ + [ + 'date' => isset( $invoice_data['payment.date'] ) ? date( 'Y-m-d', strtotime( $invoice_data['payment.date'] ) ) : date( 'y-m-d' ), + 'transaction_reference' => $this->getString( $invoice_data, 'payment.transaction_reference' ), + 'amount' => $transformed['amount'] - $transformed['balance'], + ], + ]; + } + + $line_items = []; + foreach ( $line_items_data as $record ) { + $line_items[] = [ + 'quantity' => $this->getFloat( $record, 'item.quantity' ), + 'cost' => $this->getFloat( $record, 'item.cost' ), + 'product_key' => $this->getString( $record, 'item.product_key' ), + 'notes' => $this->getString( $record, 'item.notes' ), + 'discount' => $this->getFloat( $record, 'item.discount' ), + 'is_amount_discount' => filter_var( $this->getString( $record, 'item.is_amount_discount' ), FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE ), + 'tax_name1' => $this->getString( $record, 'item.tax_name1' ), + 'tax_rate1' => $this->getFloat( $record, 'item.tax_rate1' ), + 'tax_name2' => $this->getString( $record, 'item.tax_name2' ), + 'tax_rate2' => $this->getFloat( $record, 'item.tax_rate2' ), + 'tax_name3' => $this->getString( $record, 'item.tax_name3' ), + 'tax_rate3' => $this->getFloat( $record, 'item.tax_rate3' ), + 'custom_value1' => $this->getString( $record, 'item.custom_value1' ), + 'custom_value2' => $this->getString( $record, 'item.custom_value2' ), + 'custom_value3' => $this->getString( $record, 'item.custom_value3' ), + 'custom_value4' => $this->getString( $record, 'item.custom_value4' ), + 'type_id' => $this->getInvoiceTypeId( $record, 'item.type_id' ), + ]; + } + $transformed['line_items'] = $line_items; + + return $transformed; + } +} diff --git a/app/Import/Transformer/Csv/ProductTransformer.php b/app/Import/Transformer/Csv/ProductTransformer.php new file mode 100644 index 000000000000..f89bdc385bec --- /dev/null +++ b/app/Import/Transformer/Csv/ProductTransformer.php @@ -0,0 +1,46 @@ + $this->company->id, + 'product_key' => $this->getString($data, 'product.product_key'), + 'notes' => $this->getString($data, 'product.notes'), + 'cost' => $this->getFloat($data, 'product.cost'), + 'price' => $this->getFloat($data, 'product.price'), + 'quantity' => $this->getFloat($data, 'product.quantity'), + 'tax_name1' => $this->getString($data, 'product.tax_name1'), + 'tax_rate1' => $this->getFloat($data, 'product.tax_rate1'), + 'tax_name2' => $this->getString($data, 'product.tax_name2'), + 'tax_rate2' => $this->getFloat($data, 'product.tax_rate2'), + 'tax_name3' => $this->getString($data, 'product.tax_name3'), + 'tax_rate3' => $this->getFloat($data, 'product.tax_rate3'), + 'custom_value1' => $this->getString($data, 'product.custom_value1'), + 'custom_value2' => $this->getString($data, 'product.custom_value2'), + 'custom_value3' => $this->getString($data, 'product.custom_value3'), + 'custom_value4' => $this->getString($data, 'product.custom_value4'), + ]; + } +} diff --git a/app/Jobs/Import/CSVIngest.php b/app/Jobs/Import/CSVIngest.php new file mode 100644 index 000000000000..c30871e29958 --- /dev/null +++ b/app/Jobs/Import/CSVIngest.php @@ -0,0 +1,99 @@ +company = $company; + $this->request = $request; + $this->hash = $request['hash']; + $this->import_type = $request['import_type']; + $this->skip_header = $request['skip_header'] ?? null; + $this->column_map = + ! empty( $request['column_map'] ) ? + array_combine( array_keys( $request['column_map'] ), array_column( $request['column_map'], 'mapping' ) ) : null; + } + + /** + * Execute the job. + * + * + * @return void + */ + public function handle() { + + MultiDB::setDb( $this->company->db ); + + $engine = $this->bootEngine($this->import_type); + + foreach ( [ 'client', 'product', 'invoice', 'payment', 'vendor', 'expense' ] as $entity ) { + + $engine->import($entity); + + } + + } + + private function bootEngine(string $import_type) + { + switch ($import_type) { + case 'csv': + return new Csv( $this->request, $this->company); + break; + case 'waveaccounting': + return new Wave( $this->request, $this->company); + break; + case 'invoicely': + return new Invoicely( $this->request, $this->company); + break; + case 'invoice2go': + return new Invoice2Go( $this->request, $this->company); + break; + case 'zoho': + return new Zoho( $this->request, $this->company); + break; + case 'freshbooks': + return new Freshbooks( $this->request, $this->company); + break; + default: + // code... + break; + } + } +} \ No newline at end of file diff --git a/app/Jobs/Ninja/CompanySizeCheck.php b/app/Jobs/Ninja/CompanySizeCheck.php index ea1dd2efaa84..79a530d1eb01 100644 --- a/app/Jobs/Ninja/CompanySizeCheck.php +++ b/app/Jobs/Ninja/CompanySizeCheck.php @@ -54,16 +54,31 @@ class CompanySizeCheck implements ShouldQueue private function check() { - Company::cursor()->each(function ($company) { - if ($company->invoices()->count() > 500 || $company->products()->count() > 500 || $company->clients()->count() > 500) { - - nlog("Marking company {$company->id} as large"); - - $company->is_large = true; - $company->save(); - } + Company::where('is_large', false)->withCount(['invoices','clients','products'])->cursor()->each(function ($company){ - }); + if ($company->invoices_count > 500 || $company->products_count > 500 || $company->clients_count > 500) + { + + nlog("Marking company {$company->id} as large"); + + $company->account->companies()->update(['is_large' => true]); + + } + + + }); + + // Company::where('is_large', false)->cursor()->each(function ($company) { + + // if ($company->invoices()->count() > 500 || $company->products()->count() > 500 || $company->clients()->count() > 500) { + + // nlog("Marking company {$company->id} as large"); + + // $company->account->companies->update(['is_large' => true]) + + // } + + // }); } } diff --git a/app/Jobs/RecurringInvoice/SendRecurring.php b/app/Jobs/RecurringInvoice/SendRecurring.php index 24de8b99f883..d21724365751 100644 --- a/app/Jobs/RecurringInvoice/SendRecurring.php +++ b/app/Jobs/RecurringInvoice/SendRecurring.php @@ -65,10 +65,15 @@ class SendRecurring implements ShouldQueue // Generate Standard Invoice $invoice = RecurringInvoiceToInvoiceFactory::create($this->recurring_invoice, $this->recurring_invoice->client); - if($this->recurring_invoice->auto_bill == "always") + if($this->recurring_invoice->auto_bill === "always"){ $invoice->auto_bill_enabled = true; - elseif($this->recurring_invoice->auto_bill == "off") + } + elseif($this->recurring_invoice->auto_bill === "optout" || $this->recurring_invoice->auto_bill === "optin"){ + + } + elseif($this->recurring_invoice->auto_bill === "off"){ $invoice->auto_bill_enabled = false; + } $invoice->date = now()->format('Y-m-d'); $invoice->due_date = $this->recurring_invoice->calculateDueDate(now()->format('Y-m-d')); diff --git a/app/PaymentDrivers/Authorize/AuthorizeCreateCustomer.php b/app/PaymentDrivers/Authorize/AuthorizeCreateCustomer.php index af39afbe3249..e170a5042968 100644 --- a/app/PaymentDrivers/Authorize/AuthorizeCreateCustomer.php +++ b/app/PaymentDrivers/Authorize/AuthorizeCreateCustomer.php @@ -44,7 +44,7 @@ class AuthorizeCreateCustomer $this->authorize->init(); // Create the Bill To info for new payment type - $contact = $this->client->primary_contact()->first(); + $contact = $this->client->primary_contact()->first() ?: $this->client->contacts()->first(); $refId = 'ref'.time(); // Create a new CustomerProfileType and add the payment profile object diff --git a/app/PaymentDrivers/CheckoutCom/Utilities.php b/app/PaymentDrivers/CheckoutCom/Utilities.php index eddc38495eba..3697f3d8c5c5 100644 --- a/app/PaymentDrivers/CheckoutCom/Utilities.php +++ b/app/PaymentDrivers/CheckoutCom/Utilities.php @@ -62,7 +62,7 @@ trait Utilities $data = [ 'payment_method' => $_payment->source['id'], - 'payment_type' => PaymentType::parseCardType(strtolower($_payment->source['scheme'])), + 'payment_type' => 12, 'amount' => $this->getParent()->payment_hash->data->raw_value, 'transaction_reference' => $_payment->id, 'gateway_type_id' => GatewayType::CREDIT_CARD, diff --git a/app/PaymentDrivers/Stripe/Jobs/PaymentIntentWebhook.php b/app/PaymentDrivers/Stripe/Jobs/PaymentIntentWebhook.php index f621e923d22b..8ca8ebe1989a 100644 --- a/app/PaymentDrivers/Stripe/Jobs/PaymentIntentWebhook.php +++ b/app/PaymentDrivers/Stripe/Jobs/PaymentIntentWebhook.php @@ -119,7 +119,8 @@ class PaymentIntentWebhook implements ShouldQueue $payment_hash = PaymentHash::where('hash', $hash)->first(); - nlog("no payment found"); + if(!$payment_hash) + return; if(optional($this->stripe_request['object']['charges']['data'][0]['metadata']['payment_hash']) && in_array('card', $this->stripe_request['object']['allowed_source_types'])) { diff --git a/app/Repositories/ActivityRepository.php b/app/Repositories/ActivityRepository.php index 6da7e24d398d..a4a771991813 100644 --- a/app/Repositories/ActivityRepository.php +++ b/app/Repositories/ActivityRepository.php @@ -54,7 +54,7 @@ class ActivityRepository extends BaseRepository $activity->token_id = $token_id; } - $activity->ip = $event_vars['ip']; + $activity->ip = $event_vars['ip'] ?: ' '; $activity->is_system = $event_vars['is_system']; $activity->save(); diff --git a/app/Services/Subscription/SubscriptionService.php b/app/Services/Subscription/SubscriptionService.php index 5b6e010d73c1..4dcf472ba0f5 100644 --- a/app/Services/Subscription/SubscriptionService.php +++ b/app/Services/Subscription/SubscriptionService.php @@ -91,7 +91,7 @@ class SubscriptionService 'invoice' => $this->encodePrimaryKey($payment_hash->fee_invoice_id), 'client' => $recurring_invoice->client->hashed_id, 'subscription' => $this->subscription->hashed_id, - 'contact' => auth('contact')->user()->hashed_id, + 'contact' => auth('contact')->user() ? auth('contact')->user()->hashed_id : $recurring_invoice->client->contacts()->first()->hashed_id, 'account_key' => $recurring_invoice->client->custom_value2, ]; diff --git a/app/Utils/Ninja.php b/app/Utils/Ninja.php index 515d47230806..95435412a9dd 100644 --- a/app/Utils/Ninja.php +++ b/app/Utils/Ninja.php @@ -119,9 +119,9 @@ class Ninja if(request()->hasHeader('Cf-Connecting-Ip')) $ip = request()->header('Cf-Connecting-Ip'); elseif(request()->hasHeader('X-Forwarded-For')) - $ip = request()->header('Cf-Connecting-Ip'); + $ip = request()->header('X-Forwarded-For'); else - $ip = request()->ip(); + $ip = request()->ip() ?: ' '; return [ 'ip' => $ip, diff --git a/config/livewire.php b/config/livewire.php index 59f8d5c4ddc7..132a3222c672 100644 --- a/config/livewire.php +++ b/config/livewire.php @@ -54,7 +54,8 @@ return [ | */ - 'asset_url' => null, + //'asset_url' => null, + 'asset_url' => env('ASSET_URL', null), /* |-------------------------------------------------------------------------- diff --git a/config/ninja.php b/config/ninja.php index 1ae6e825c011..64676a4cc41f 100644 --- a/config/ninja.php +++ b/config/ninja.php @@ -14,8 +14,8 @@ return [ 'require_https' => env('REQUIRE_HTTPS', true), 'app_url' => rtrim(env('APP_URL', ''), '/'), 'app_domain' => env('APP_DOMAIN', 'invoicing.co'), - 'app_version' => '5.3.54', - 'app_tag' => '5.3.54', + 'app_version' => '5.3.55', + 'app_tag' => '5.3.55', 'minimum_client_version' => '5.0.16', 'terms_version' => '1.0.1', 'api_secret' => env('API_SECRET', ''), diff --git a/routes/api.php b/routes/api.php index ff853ab9f400..2ae7c343f9f5 100644 --- a/routes/api.php +++ b/routes/api.php @@ -40,6 +40,7 @@ Route::group(['middleware' => ['throttle:300,1', 'api_db', 'token_auth', 'locale Route::resource('clients', 'ClientController'); // name = (clients. index / create / show / update / destroy / edit Route::put('clients/{client}/adjust_ledger', 'ClientController@adjustLedger')->name('clients.adjust_ledger'); Route::put('clients/{client}/upload', 'ClientController@upload')->name('clients.upload'); + Route::post('clients/{client}/purge', 'ClientController@purge')->name('clients.purge')->middleware('password_protected'); Route::post('clients/bulk', 'ClientController@bulk')->name('clients.bulk'); Route::post('filters/{entity}', 'FilterController@index')->name('filters'); @@ -212,7 +213,7 @@ Route::group(['middleware' => ['throttle:300,1', 'api_db', 'token_auth', 'locale Route::resource('subscriptions', 'SubscriptionController'); Route::post('subscriptions/bulk', 'SubscriptionController@bulk')->name('subscriptions.bulk'); Route::get('statics', 'StaticController'); - Route::post('apple_pay/upload_file','ApplyPayController@upload'); + // Route::post('apple_pay/upload_file','ApplyPayController@upload'); }); diff --git a/tests/Feature/CompanyTest.php b/tests/Feature/CompanyTest.php index af604cbb67ad..ed3e3f7cc6b9 100644 --- a/tests/Feature/CompanyTest.php +++ b/tests/Feature/CompanyTest.php @@ -50,6 +50,9 @@ class CompanyTest extends TestCase { $this->withoutMiddleware(PasswordProtection::class); + $cc = Company::first(); + $cc->delete(); + $response = $this->withHeaders([ 'X-API-SECRET' => config('ninja.api_secret'), 'X-API-TOKEN' => $this->token, diff --git a/tests/Feature/Import/CSV/BaseTransformerTest.php b/tests/Feature/Import/CSV/BaseTransformerTest.php new file mode 100644 index 000000000000..f00cdf8382cd --- /dev/null +++ b/tests/Feature/Import/CSV/BaseTransformerTest.php @@ -0,0 +1,345 @@ +withoutMiddleware( + ThrottleRequests::class + ); + config(['database.default' => config('ninja.db.default')]); + + // $this->faker = \Faker\Factory::create(); + + $this->makeTestData(); + + $this->withoutExceptionHandling(); + } + + public function testGetString() + { + $base_transformer = new BaseTransformer($this->company); + + $data = [ + 'key' => 'value' + ]; + + $field = 'key'; + + $this->assertEquals('value', $base_transformer->getString($data, $field)); + + } + + public function testGetCurrencyCode() + { + $base_transformer = new BaseTransformer($this->company); + + $code = ['client.currency_id' => "USD"]; + + $currency_id = $base_transformer->getCurrencyByCode($code); + + $this->assertEquals(1, $currency_id); + } + + public function testGetClient() + { + $base_transformer = new BaseTransformer($this->company); + + $client = Client::factory()->create([ + 'user_id' => $this->user->id, + 'company_id' => $this->company->id, + 'id_number' => 'hit', + 'name' => 'magic ', + ]); + + $contact = ClientContact::factory()->create([ + 'user_id' => $this->user->id, + 'client_id' => $client->id, + 'company_id' => $this->company->id, + 'is_primary' => 1, + 'send_email' => true, + 'email' => 'test@gmail.com' + ]); + + + $this->assertEquals($client->id, $base_transformer->getClient('hit', 'null')); + $this->assertEquals($client->id, $base_transformer->getClient('magic', 'null')); + $this->assertEquals($client->id, $base_transformer->getClient('nomagic', 'test@gmail.com')); + $this->assertEquals($client->id, $base_transformer->getClient(null, 'test@gmail.com')); + $this->assertNull($base_transformer->getClient('null', 'notest@gmail.com')); + + $this->assertEquals($client->id, $base_transformer->getClientId(' magic')); + $this->assertEquals($client->id, $base_transformer->getClientId('Magic ')); + + } + + public function testGetContact() + { + $base_transformer = new BaseTransformer($this->company); + + $client = Client::factory()->create([ + 'user_id' => $this->user->id, + 'company_id' => $this->company->id, + 'id_number' => 'hit', + 'name' => 'magic ', + ]); + + $contact = ClientContact::factory()->create([ + 'user_id' => $this->user->id, + 'client_id' => $client->id, + 'company_id' => $this->company->id, + 'is_primary' => 1, + 'send_email' => true, + 'email' => 'test@gmail.com' + ]); + + $this->assertEquals($contact->id, $base_transformer->getContact('TeSt@gmail.com')->id); + $this->assertEquals($contact->id, $base_transformer->getContact('TeSt@gmail.com ')->id); + $this->assertEquals($contact->id, $base_transformer->getContact('TeSt@gmaiL.com')->id); + + } + + public function testHasClient() + { + $base_transformer = new BaseTransformer($this->company); + + $client = Client::factory()->create([ + 'user_id' => $this->user->id, + 'company_id' => $this->company->id, + 'id_number' => 'hit', + 'name' => 'maGic ', + ]); + + + $this->assertTrue($base_transformer->hasClient("magic")); + $this->assertTrue($base_transformer->hasClient("Magic")); + $this->assertTrue($base_transformer->hasClient("Ma gi c ")); + + } + + public function testHasVendor() + { + $base_transformer = new BaseTransformer($this->company); + + $client = Vendor::factory()->create([ + 'user_id' => $this->user->id, + 'company_id' => $this->company->id, + 'id_number' => 'hit', + 'name' => 'maGic ', + ]); + + + $this->assertTrue($base_transformer->hasVendor("magic")); + $this->assertTrue($base_transformer->hasVendor("Magic")); + $this->assertTrue($base_transformer->hasVendor("Ma gi c ")); + + } + + public function testHasProduct() + { + $base_transformer = new BaseTransformer($this->company); + + $client = Product::factory()->create([ + 'user_id' => $this->user->id, + 'company_id' => $this->company->id, + 'product_key' => 'HiT ', + ]); + + + $this->assertTrue($base_transformer->hasProduct("hit")); + $this->assertTrue($base_transformer->hasProduct(" hIt")); + $this->assertTrue($base_transformer->hasProduct(" h i T ")); + + } + + + + public function testGetCountryId() + { + $base_transformer = new BaseTransformer($this->company); + + $this->assertEquals(840, $base_transformer->getCountryId("us")); + $this->assertEquals(840, $base_transformer->getCountryId("US")); + $this->assertEquals(840, $base_transformer->getCountryId("United States")); + + } + + + public function testGetTaxRate() + { + $base_transformer = new BaseTransformer($this->company); + + $client = TaxRate::factory()->create([ + 'user_id' => $this->user->id, + 'company_id' => $this->company->id, + 'rate' => '10', + 'name' => 'GST' + ]); + + $this->assertEquals(10, $base_transformer->getTaxRate("gst")); + $this->assertEquals(10, $base_transformer->getTaxRate(" GST")); + $this->assertEquals(10, $base_transformer->getTaxRate(" gS t ")); + + } + + + public function testGetTaxName() + { + $base_transformer = new BaseTransformer($this->company); + + $client = TaxRate::factory()->create([ + 'user_id' => $this->user->id, + 'company_id' => $this->company->id, + 'rate' => '17.5', + 'name' => 'VAT' + ]); + + $this->assertEquals("VAT", $base_transformer->getTaxName("vat")); + $this->assertEquals("VAT", $base_transformer->getTaxName(" VaT")); + $this->assertEquals("VAT", $base_transformer->getTaxName(" va T ")); + + } + + public function testGetInvoiceId() + { + $base_transformer = new BaseTransformer($this->company); + + $invoice = Invoice::factory()->create([ + 'user_id' => $this->user->id, + 'company_id' => $this->company->id, + 'client_id' => $this->client->id, + 'number' => 'trick_number_123', + ]); + + $this->assertEquals($invoice->id, $base_transformer->getInvoiceId("TRICK_number_123")); + $this->assertEquals($invoice->id, $base_transformer->getInvoiceId(" TRICK_number_123")); + $this->assertEquals($invoice->id, $base_transformer->getInvoiceId(" TRICK_number_123 ")); + + } + + + + public function testHasInvoiceWithNumber() + { + $base_transformer = new BaseTransformer($this->company); + + $client = Invoice::factory()->create([ + 'user_id' => $this->user->id, + 'company_id' => $this->company->id, + 'client_id' => $this->client->id, + 'number' => 'tricky_number_123', + ]); + + $this->assertTrue($base_transformer->hasInvoice("TRICKY_number_123")); + $this->assertTrue($base_transformer->hasInvoice(" TRICKY_number_123")); + $this->assertTrue($base_transformer->hasInvoice(" TRICKY_number_123 ")); + + } + public function testInvoiceClientId() + { + $base_transformer = new BaseTransformer($this->company); + + $client = Invoice::factory()->create([ + 'user_id' => $this->user->id, + 'company_id' => $this->company->id, + 'client_id' => $this->client->id, + 'number' => 'tricky_number_123', + ]); + + $this->assertEquals($this->client->id, $base_transformer->getInvoiceClientId("TRICKY_number_123")); + $this->assertEquals($this->client->id, $base_transformer->getInvoiceClientId(" TRICKY_number_123")); + $this->assertEquals($this->client->id, $base_transformer->getInvoiceClientId(" TRICKY_number_123 ")); + + } + + public function testGetVendorId() + { + $base_transformer = new BaseTransformer($this->company); + + $vendor = Vendor::factory()->create([ + 'user_id' => $this->user->id, + 'company_id' => $this->company->id, + 'id_number' => 'hit', + 'name' => 'maGic ', + ]); + + + $this->assertEquals($vendor->id, $base_transformer->getVendorId("magic")); + $this->assertEquals($vendor->id, $base_transformer->getVendorId("Magic")); + $this->assertEquals($vendor->id, $base_transformer->getVendorId("Ma gi c ")); + + } + + + + + // public function testClientCsvImport() + // { + // $csv = file_get_contents(base_path().'/tests/Feature/Import/clients.csv'); + // $hash = Str::random(32); + // $column_map = [ + // 1 => 'client.balance', + // 2 => 'client.paid_to_date', + // 0 => 'client.name', + // 19 => 'client.currency_id', + // 20 => 'client.public_notes', + // 21 => 'client.private_notes', + // 22 => 'contact.first_name', + // 23 => 'contact.last_name', + // ]; + + // $data = [ + // 'hash' => $hash, + // 'column_map' => [ 'client' => [ 'mapping' => $column_map ] ], + // 'skip_header' => true, + // 'import_type' => 'csv', + // ]; + + // $pre_import = Client::count(); + + // Cache::put( $hash . '-client', base64_encode( $csv ), 360 ); + + // CSVImport::dispatchNow( $data, $this->company ); + + // $this->assertGreaterThan( $pre_import, Client::count() ); + // } + +} diff --git a/tests/Feature/Import/CSV/CsvImportTest.php b/tests/Feature/Import/CSV/CsvImportTest.php new file mode 100644 index 000000000000..76c2631bba4b --- /dev/null +++ b/tests/Feature/Import/CSV/CsvImportTest.php @@ -0,0 +1,136 @@ +withoutMiddleware( + ThrottleRequests::class + ); + + config(['database.default' => config('ninja.db.default')]); + + $this->makeTestData(); + + $this->withoutExceptionHandling(); + } + + public function testCsvFeature() + { + $csv = file_get_contents(base_path().'/tests/Feature/Import/clients.csv'); + $hash = Str::random(32); + $column_map = [ + 1 => 'client.balance', + 2 => 'client.paid_to_date', + 0 => 'client.name', + 19 => 'client.currency_id', + 20 => 'client.public_notes', + 21 => 'client.private_notes', + 22 => 'contact.first_name', + 23 => 'contact.last_name', + 24 => 'contact.email', + ]; + + $data = [ + 'hash' => $hash, + 'column_map' => [ 'client' => [ 'mapping' => $column_map ] ], + 'skip_header' => true, + 'import_type' => 'csv', + ]; + + Cache::put( $hash . '-client', base64_encode( $csv ), 360 ); + + $csv_importer = new Csv($data, $this->company); + + $this->assertInstanceOf(Csv::class, $csv_importer); + + $csv_importer->import('client'); + + $base_transformer = new BaseTransformer($this->company); + + $this->assertTrue($base_transformer->hasClient("Ludwig Krajcik DVM")); + + $client_id = $base_transformer->getClient("Ludwig Krajcik DVM", null); + + $c = Client::find($client_id); + + $this->assertEquals($client_id, $c->id); + + $client_id = $base_transformer->getClient("a non existent clent", "brook59@example.org"); + + $this->assertEquals($client_id, $c->id); + + } + +} + + + // public function testClientCsvImport() + // { + // $csv = file_get_contents(base_path().'/tests/Feature/Import/clients.csv'); + // $hash = Str::random(32); + // $column_map = [ + // 1 => 'client.balance', + // 2 => 'client.paid_to_date', + // 0 => 'client.name', + // 19 => 'client.currency_id', + // 20 => 'client.public_notes', + // 21 => 'client.private_notes', + // 22 => 'contact.first_name', + // 23 => 'contact.last_name', + // ]; + + // $data = [ + // 'hash' => $hash, + // 'column_map' => [ 'client' => [ 'mapping' => $column_map ] ], + // 'skip_header' => true, + // 'import_type' => 'csv', + // ]; + + // $pre_import = Client::count(); + + // Cache::put( $hash . '-client', base64_encode( $csv ), 360 ); + + // CSVImport::dispatchNow( $data, $this->company ); + + // $this->assertGreaterThan( $pre_import, Client::count() ); + // } diff --git a/tests/MockAccountData.php b/tests/MockAccountData.php index b2a6276ad52c..7c0155185bc9 100644 --- a/tests/MockAccountData.php +++ b/tests/MockAccountData.php @@ -177,13 +177,17 @@ trait MockAccountData } } - $this->account = Account::factory()->create(); + $this->account = Account::factory()->create([ + 'hosted_client_count' => 1000, + 'hosted_company_count' => 1000 + ]); + $this->account->num_users = 3; $this->account->save(); $this->company = Company::factory()->create([ - 'account_id' => $this->account->id, - ]); + 'account_id' => $this->account->id, + ]); $this->company->client_registration_fields = ClientRegistrationFields::generate();