company = $company; $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); Auth::login($this->company->owner(), true); auth()->user()->setCompany($this->company); $this->buildMaps(); nlog('import '.$this->import_type); foreach (['client', 'product', 'invoice', 'payment', 'vendor', 'expense'] as $entityType) { $csvData = $this->getCsvData($entityType); if (! empty($csvData)) { $importFunction = 'import'.Str::plural(Str::title($entityType)); $preTransformFunction = 'preTransform'.Str::title($this->import_type); if (method_exists($this, $preTransformFunction)) { $csvData = $this->$preTransformFunction($csvData, $entityType); } if (empty($csvData)) { continue; } if (method_exists($this, $importFunction)) { // If there's an entity-specific import function, use that. $this->$importFunction($csvData); } else { // Otherwise, use the generic import function. $this->importEntities($csvData, $entityType); } } } $data = [ 'errors' => $this->error_array, 'company' => $this->company, ]; $nmo = new NinjaMailerObject; $nmo->mailable = new ImportCompleted($this->company, $data); $nmo->company = $this->company; $nmo->settings = $this->company->settings; $nmo->to_user = $this->company->owner(); NinjaMailerJob::dispatch($nmo); } ////////////////////////////////////////////////////////////////////////////////////////////////////////////// private function preTransformCsv($csvData, $entityType) { if (empty($this->column_map[$entityType])) { return false; } if ($this->skip_header) { array_shift($csvData); } //sort the array by key $keys = $this->column_map[$entityType]; ksort($keys); $csvData = array_map(function ($row) use ($keys) { return array_combine($keys, array_intersect_key($row, $keys)); }, $csvData); if ($entityType === 'invoice') { $csvData = $this->groupInvoices($csvData, 'invoice.number'); } return $csvData; } private function preTransformFreshbooks($csvData, $entityType) { $csvData = $this->mapCSVHeaderToKeys($csvData); if ($entityType === 'invoice') { $csvData = $this->groupInvoices($csvData, 'Invoice #'); } return $csvData; } private function preTransformInvoicely($csvData, $entityType) { $csvData = $this->mapCSVHeaderToKeys($csvData); return $csvData; } private function preTransformInvoice2go($csvData, $entityType) { $csvData = $this->mapCSVHeaderToKeys($csvData); return $csvData; } private function preTransformZoho($csvData, $entityType) { $csvData = $this->mapCSVHeaderToKeys($csvData); if ($entityType === 'invoice') { $csvData = $this->groupInvoices($csvData, 'Invoice Number'); } return $csvData; } private function preTransformWaveaccounting($csvData, $entityType) { $csvData = $this->mapCSVHeaderToKeys($csvData); if ($entityType === 'invoice') { $csvData = $this->groupInvoices($csvData, 'Invoice Number'); } return $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; } private function mapCSVHeaderToKeys($csvData) { $keys = array_shift($csvData); return array_map(function ($values) use ($keys) { return array_combine($keys, $values); }, $csvData); } ////////////////////////////////////////////////////////////////////////////////////////////////////////////// private function importInvoices($invoices) { $invoice_transformer = $this->getTransformer('invoice'); /** @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]; } } } private function actionInvoiceStatus($invoice, $invoice_data, $invoice_repository) { if (! empty($invoice_data['archived'])) { $invoice_repository->archive($invoice); $invoice->fresh(); } if (! empty($invoice_data['viewed'])) { $invoice = $invoice->service()->markViewed()->save(); } if ($invoice->status_id === Invoice::STATUS_DRAFT) { } elseif ($invoice->status_id === Invoice::STATUS_SENT) { $invoice = $invoice->service()->markSent()->save(); } elseif ($invoice->status_id <= Invoice::STATUS_SENT && $invoice->amount > 0) { if ($invoice->balance <= 0) { $invoice->status_id = Invoice::STATUS_PAID; $invoice->save(); } elseif ($invoice->balance != $invoice->amount) { $invoice->status_id = Invoice::STATUS_PARTIAL; $invoice->save(); } } return $invoice; } private function importEntities($records, $entity_type) { $entity_type = Str::slug($entity_type, '_'); $formatted_entity_type = Str::title($entity_type); $request_name = "\\App\\Http\\Requests\\${formatted_entity_type}\\Store${formatted_entity_type}Request"; $repository_name = '\\App\\Repositories\\'.$formatted_entity_type.'Repository'; $factoryName = '\\App\\Factory\\'.$formatted_entity_type.'Factory'; /** @var BaseRepository $repository */ $repository = app()->make($repository_name); $repository->import_mode = true; $transformer = $this->getTransformer($entity_type); foreach ($records as $record) { try { $entity = $transformer->transform($record); /** @var \App\Http\Requests\Request $request */ // $request = new $request_name(); // $request->prepareForValidation(); // Pass entity data to request so it can be validated // $request->query = $request->request = new ParameterBag( $entity ); // $validator = Validator::make( $entity, $request->rules() ); $validator = $request_name::runFormRequest($entity); if ($validator->fails()) { $this->error_array[$entity_type][] = [$entity_type => $record, 'error' => $validator->errors()->all()]; } else { $entity = $repository->save( array_diff_key($entity, ['user_id' => false]), $factoryName::create($this->company->id, $this->getUserIDForRecord($entity))); $entity->save(); if (method_exists($this, 'add'.$formatted_entity_type.'ToMaps')) { $this->{'add'.$formatted_entity_type.'ToMaps'}($entity); } } } 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]; } } } /** * @param $entity_type * * @return BaseTransformer */ private function getTransformer($entity_type) { $formatted_entity_type = Str::title($entity_type); $formatted_import_type = Str::title($this->import_type); $transformer_name = '\\App\\Import\\Transformers\\'.$formatted_import_type.'\\'.$formatted_entity_type.'Transformer'; return new $transformer_name($this->maps); } ////////////////////////////////////////////////////////////////////////////////////////////////////////////// private function buildMaps() { $this->maps = [ 'company' => $this->company, 'client' => [], 'contact' => [], 'invoice' => [], 'invoice_client' => [], 'product' => [], 'countries' => [], 'countries2' => [], 'currencies' => [], 'client_ids' => [], 'invoice_ids' => [], 'vendors' => [], 'expense_categories' => [], 'payment_types' => [], 'tax_rates' => [], 'tax_names' => [], ]; Client::where('company_id', $this->company->id)->cursor()->each(function ($client) { $this->addClientToMaps($client); }); ClientContact::where('company_id', $this->company->id)->cursor()->each(function ($contact) { $this->addContactToMaps($contact); }); Invoice::where('company_id', $this->company->id)->cursor()->each(function ($invoice) { $this->addInvoiceToMaps($invoice); }); Product::where('company_id', $this->company->id)->cursor()->each(function ($product) { $this->addProductToMaps($product); }); Project::where('company_id', $this->company->id)->cursor()->each(function ($project) { $this->addProjectToMaps($project); }); Country::all()->each(function ($country) { $this->maps['countries'][strtolower($country->name)] = $country->id; $this->maps['countries2'][strtolower($country->iso_3166_2)] = $country->id; }); Currency::all()->each(function ($currency) { $this->maps['currencies'][strtolower($currency->code)] = $currency->id; }); PaymentType::all()->each(function ($payment_type) { $this->maps['payment_types'][strtolower($payment_type->name)] = $payment_type->id; }); Vendor::where('company_id', $this->company->id)->cursor()->each(function ($vendor) { $this->addVendorToMaps($vendor); }); ExpenseCategory::where('company_id', $this->company->id)->cursor()->each(function ($category) { $this->addExpenseCategoryToMaps($category); }); TaxRate::where('company_id', $this->company->id)->cursor()->each(function ($taxRate) { $name = trim(strtolower($taxRate->name)); $this->maps['tax_rates'][$name] = $taxRate->rate; $this->maps['tax_names'][$name] = $taxRate->name; }); } /** * @param Invoice $invoice */ private function addInvoiceToMaps(Invoice $invoice) { if ($number = strtolower(trim($invoice->number))) { $this->maps['invoices'][$number] = $invoice; $this->maps['invoice'][$number] = $invoice->id; $this->maps['invoice_client'][$number] = $invoice->client_id; $this->maps['invoice_ids'][$invoice->public_id] = $invoice->id; } } /** * @param Client $client */ private function addClientToMaps(Client $client) { if ($name = strtolower(trim($client->name))) { $this->maps['client'][$name] = $client->id; $this->maps['client_ids'][$client->public_id] = $client->id; } if ($client->contacts->count()) { $contact = $client->contacts[0]; if ($email = strtolower(trim($contact->email))) { $this->maps['client'][$email] = $client->id; } if ($name = strtolower(trim($contact->first_name.' '.$contact->last_name))) { $this->maps['client'][$name] = $client->id; } $this->maps['client_ids'][$client->public_id] = $client->id; } } /** * @param ClientContact $contact */ private function addContactToMaps(ClientContact $contact) { if ($key = strtolower(trim($contact->email))) { $this->maps['contact'][$key] = $contact; } } /** * @param Product $product */ private function addProductToMaps(Product $product) { if ($key = strtolower(trim($product->product_key))) { $this->maps['product'][$key] = $product; } } /** * @param Project $project */ private function addProjectToMaps(Project $project) { if ($key = strtolower(trim($project->name))) { $this->maps['project'][$key] = $project; } } private function addVendorToMaps(Vendor $vendor) { $this->maps['vendor'][strtolower($vendor->name)] = $vendor->id; } private function addExpenseCategoryToMaps(ExpenseCategory $category) { if ($name = strtolower($category->name)) { $this->maps['expense_category'][$name] = $category->id; } } private function getUserIDForRecord($record) { if (! empty($record['user_id'])) { return $this->findUser($record['user_id']); } else { return $this->company->owner()->id; } } private 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; } } private function getCsvData($entityType) { $base64_encoded_csv = Cache::pull($this->hash.'-'.$entityType); 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') { $firstCell = $headers[0]; if (strstr($firstCell, config('ninja.app_name'))) { array_shift($data); // Invoice Ninja... array_shift($data); // array_shift($data); // Enitty Type Header } } } return $data; } }