diff --git a/app/Http/Controllers/Import/IndexController.php b/app/Http/Controllers/Import/IndexController.php index 639b5bb152..4dad6d55ca 100644 --- a/app/Http/Controllers/Import/IndexController.php +++ b/app/Http/Controllers/Import/IndexController.php @@ -24,7 +24,6 @@ namespace FireflyIII\Http\Controllers\Import; use FireflyIII\Exceptions\FireflyException; use FireflyIII\Http\Controllers\Controller; -use FireflyIII\Http\Middleware\IsDemoUser; use FireflyIII\Import\Prerequisites\PrerequisitesInterface; use FireflyIII\Repositories\ImportJob\ImportJobRepositoryInterface; use View; @@ -54,7 +53,6 @@ class IndexController extends Controller return $next($request); } ); - $this->middleware(IsDemoUser::class)->except(['index']); } /** @@ -68,11 +66,19 @@ class IndexController extends Controller */ public function create(string $importProvider) { + if ( + !(bool)config('app.debug') + && !(bool)config(sprintf('import.enabled.%s', $importProvider)) === true + && !\in_array(config('app.env'), ['demo', 'testing']) + ) { + throw new FireflyException(sprintf('Import using provider "%s" is currently not available.', $importProvider)); // @codeCoverageIgnore + } + $importJob = $this->repository->create($importProvider); // if job provider has no prerequisites: if (!(bool)config(sprintf('import.has_prereq.%s', $importProvider))) { - + // @codeCoverageIgnoreStart // if job provider also has no configuration: if (!(bool)config(sprintf('import.has_config.%s', $importProvider))) { $this->repository->updateStatus($importJob, 'ready_to_run'); @@ -85,6 +91,7 @@ class IndexController extends Controller // redirect to job configuration. return redirect(route('import.job.configuration.index', [$importJob->key])); + // @codeCoverageIgnoreEnd } // if need to set prerequisites, do that first. @@ -122,7 +129,7 @@ class IndexController extends Controller $config = config('import.enabled'); $providers = []; foreach ($config as $name => $enabled) { - if ($enabled || (bool)config('app.debug')) { + if ($enabled || (bool)config('app.debug') || \in_array(config('app.env'), ['demo', 'testing'])) { $providers[$name] = []; } } @@ -147,110 +154,4 @@ class IndexController extends Controller return view('import.index', compact('subTitle', 'subTitleIcon', 'providers')); } - // - // /** - // * @param Request $request - // * @param string $bank - // * - // * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector - // */ - // public function reset(Request $request, string $bank) - // { - // if ($bank === 'bunq') { - // // remove bunq related preferences. - // Preferences::delete('bunq_api_key'); - // Preferences::delete('bunq_server_public_key'); - // Preferences::delete('bunq_private_key'); - // Preferences::delete('bunq_public_key'); - // Preferences::delete('bunq_installation_token'); - // Preferences::delete('bunq_installation_id'); - // Preferences::delete('bunq_device_server_id'); - // Preferences::delete('external_ip'); - // - // } - // - // if ($bank === 'spectre') { - // // remove spectre related preferences: - // Preferences::delete('spectre_client_id'); - // Preferences::delete('spectre_app_secret'); - // Preferences::delete('spectre_service_secret'); - // Preferences::delete('spectre_app_id'); - // Preferences::delete('spectre_secret'); - // Preferences::delete('spectre_private_key'); - // Preferences::delete('spectre_public_key'); - // Preferences::delete('spectre_customer'); - // } - // - // Preferences::mark(); - // $request->session()->flash('info', (string)trans('firefly.settings_reset_for_' . $bank)); - // - // return redirect(route('import.index')); - // - // } - - // /** - // * @param ImportJob $job - // * - // * @return \Illuminate\Http\JsonResponse - // * - // * @throws FireflyException - // */ - // public function start(ImportJob $job) - // { - // $type = $job->file_type; - // $key = sprintf('import.routine.%s', $type); - // $className = config($key); - // if (null === $className || !class_exists($className)) { - // throw new FireflyException(sprintf('Cannot find import routine class for job of type "%s".', $type)); // @codeCoverageIgnore - // } - // - // /** @var RoutineInterface $routine */ - // $routine = app($className); - // $routine->setJob($job); - // $result = $routine->run(); - // - // if ($result) { - // return response()->json(['run' => 'ok']); - // } - // - // throw new FireflyException('Job did not complete successfully. Please review the log files.'); - // } - - - // /** - // * Generate a JSON file of the job's configuration and send it to the user. - // * - // * @param ImportJob $job - // * - // * @return LaravelResponse - // */ - // public function download(ImportJob $job) - // { - // Log::debug('Now in download()', ['job' => $job->key]); - // $config = $job->configuration; - // - // // This is CSV import specific: - // $config['column-roles-complete'] = false; - // $config['column-mapping-complete'] = false; - // $config['initial-config-complete'] = false; - // $config['has-file-upload'] = false; - // $config['delimiter'] = "\t" === $config['delimiter'] ? 'tab' : $config['delimiter']; - // unset($config['stage']); - // - // $result = json_encode($config, JSON_PRETTY_PRINT); - // $name = sprintf('"%s"', addcslashes('import-configuration-' . date('Y-m-d') . '.json', '"\\')); - // - // /** @var LaravelResponse $response */ - // $response = response($result, 200); - // $response->header('Content-disposition', 'attachment; filename=' . $name) - // ->header('Content-Type', 'application/json') - // ->header('Content-Description', 'File Transfer') - // ->header('Connection', 'Keep-Alive') - // ->header('Expires', '0') - // ->header('Cache-Control', 'must-revalidate, post-check=0, pre-check=0') - // ->header('Pragma', 'public') - // ->header('Content-Length', \strlen($result)); - // - // return $response; - // } } diff --git a/app/Http/Controllers/Import/JobConfigurationController.php b/app/Http/Controllers/Import/JobConfigurationController.php index b2878f59ef..1d61ae5612 100644 --- a/app/Http/Controllers/Import/JobConfigurationController.php +++ b/app/Http/Controllers/Import/JobConfigurationController.php @@ -70,10 +70,10 @@ class JobConfigurationController extends Controller public function index(ImportJob $importJob) { // catch impossible status: - $allowed = ['has_prereq', 'need_job_config', 'has_config']; - if (null !== $importJob && !in_array($importJob->status, $allowed)) { - Log::error('Job is not new but wants to do prerequisites'); + $allowed = ['has_prereq', 'need_job_config']; + if (null !== $importJob && !\in_array($importJob->status, $allowed, true)) { session()->flash('error', trans('import.bad_job_status', ['status' => $importJob->status])); + return redirect(route('import.index')); } @@ -82,10 +82,12 @@ class JobConfigurationController extends Controller // if provider has no config, just push it through: $importProvider = $importJob->provider; if (!(bool)config(sprintf('import.has_config.%s', $importProvider))) { + // @codeCoverageIgnoreStart Log::debug('Job needs no config, is ready to run!'); - $this->repository->updateStatus($importJob ,'ready_to_run'); + $this->repository->updateStatus($importJob, 'ready_to_run'); return redirect(route('import.job.status.index', [$importJob->key])); + // @codeCoverageIgnoreEnd } // create configuration class: @@ -120,10 +122,10 @@ class JobConfigurationController extends Controller public function post(Request $request, ImportJob $importJob) { // catch impossible status: - $allowed = ['has_prereq', 'need_job_config', 'has_config']; - if (null !== $importJob && !in_array($importJob->status, $allowed)) { - Log::error('Job is not new but wants to do prerequisites'); + $allowed = ['has_prereq', 'need_job_config']; + if (null !== $importJob && !\in_array($importJob->status, $allowed, true)) { session()->flash('error', trans('import.bad_job_status', ['status' => $importJob->status])); + return redirect(route('import.index')); } diff --git a/app/Http/Controllers/Import/JobStatusController.php b/app/Http/Controllers/Import/JobStatusController.php index d091b50200..7faeae86e5 100644 --- a/app/Http/Controllers/Import/JobStatusController.php +++ b/app/Http/Controllers/Import/JobStatusController.php @@ -32,7 +32,6 @@ use FireflyIII\Models\ImportJob; use FireflyIII\Repositories\ImportJob\ImportJobRepositoryInterface; use Illuminate\Http\JsonResponse; use Log; -use Symfony\Component\Debug\Exception\FatalThrowableError; /** * Class JobStatusController @@ -68,19 +67,6 @@ class JobStatusController extends Controller */ public function index(ImportJob $importJob) { - // jump away depending on job status: - if ($importJob->status === 'has_prereq') { - // TODO back to configuration. - } - - if ($importJob->status === 'errored') { - // TODO to error screen - } - - if ($importJob->status === 'finished') { - // TODO to finished screen. - } - $subTitleIcon = 'fa-gear'; $subTitle = trans('import.job_status_breadcrumb', ['key' => $importJob->key]); @@ -94,42 +80,44 @@ class JobStatusController extends Controller */ public function json(ImportJob $importJob): JsonResponse { - $extendedStatus = $importJob->extended_status; - $count = \count($importJob->transactions); - $json = [ + $count = \count($importJob->transactions); + $json = [ 'status' => $importJob->status, 'errors' => $importJob->errors, 'count' => $count, 'tag_id' => $importJob->tag_id, 'tag_name' => null === $importJob->tag_id ? null : $importJob->tag->tag, - 'journals' => $extendedStatus['count'] ?? 0, 'report_txt' => trans('import.unknown_import_result'), ]; // if count is zero: + if (null !== $importJob->tag_id) { + $count = $importJob->tag->transactionJournals->count(); + } if ($count === 0) { $json['report_txt'] = trans('import.result_no_transactions'); } - if ($count === 1 && null !== $importJob->tag_id) { - $json['report_txt'] = trans('import.result_one_transaction', ['route' => route('tags.show', [$importJob->tag_id]), 'tag' => $importJob->tag->tag ]); + if ($count === 1) { + $json['report_txt'] = trans('import.result_one_transaction', ['route' => route('tags.show', [$importJob->tag_id]), 'tag' => $importJob->tag->tag]); } if ($count > 1 && null !== $importJob->tag_id) { - $json['report_txt'] = trans('import.result_many_transactions', ['count' => $count,'route' => route('tags.show', [$importJob->tag_id]), 'tag' => $importJob->tag->tag ]); + $json['report_txt'] = trans( + 'import.result_many_transactions', ['count' => $count, 'route' => route('tags.show', [$importJob->tag_id]), 'tag' => $importJob->tag->tag] + ); } return response()->json($json); } /** - * @param ImportJob $job + * @param ImportJob $importJob * * @return JsonResponse - * @throws FireflyException */ public function start(ImportJob $importJob): JsonResponse { // catch impossible status: $allowed = ['ready_to_run', 'need_job_config']; - if (null !== $importJob && !in_array($importJob->status, $allowed)) { + if (null !== $importJob && !\in_array($importJob->status, $allowed, true)) { Log::error('Job is not ready.'); return response()->json(['status' => 'NOK', 'message' => 'JobStatusController::start expects state "ready_to_run".']); @@ -139,29 +127,13 @@ class JobStatusController extends Controller $key = sprintf('import.routine.%s', $importProvider); $className = config($key); if (null === $className || !class_exists($className)) { - return response()->json(['status' => 'NOK', 'message' => sprintf('Cannot find import routine class for job of type "%s".', $importProvider)]); + // @codeCoverageIgnoreStart + return response()->json( + ['status' => 'NOK', 'message' => sprintf('Cannot find import routine class for job of type "%s".', $importProvider)] + ); + // @codeCoverageIgnoreEnd } - - // if the job is set to "provider_finished", we should be able to store transactions - // generated by the provider. - // otherwise, just continue. - if ($importJob->status === 'provider_finished') { - try { - $this->importFromJob($importJob); - } catch (FireflyException $e) { - $message = 'The import storage routine crashed: ' . $e->getMessage(); - Log::error($message); - Log::error($e->getTraceAsString()); - - // set job errored out: - $this->repository->setStatus($importJob, 'error'); - - return response()->json(['status' => 'NOK', 'message' => $message]); - } - } - - // set job to be running: $this->repository->setStatus($importJob, 'running'); @@ -170,7 +142,7 @@ class JobStatusController extends Controller $routine->setJob($importJob); try { $routine->run(); - } catch (FireflyException $e) { + } catch (FireflyException|Exception $e) { $message = 'The import routine crashed: ' . $e->getMessage(); Log::error($message); Log::error($e->getTraceAsString()); @@ -186,16 +158,20 @@ class JobStatusController extends Controller } /** - * @param ImportJob $job + * Store does three things: + * + * - Store the transactions. + * - Add them to a tag. + * + * @param ImportJob $importJob * * @return JsonResponse - * @throws FireflyException */ public function store(ImportJob $importJob): JsonResponse { // catch impossible status: - $allowed = ['provider_finished', 'storing_data']; // todo remove storing data. - if (null !== $importJob && !in_array($importJob->status, $allowed)) { + $allowed = ['provider_finished', 'storing_data']; + if (null !== $importJob && !\in_array($importJob->status, $allowed, true)) { Log::error('Job is not ready.'); return response()->json(['status' => 'NOK', 'message' => 'JobStatusController::start expects state "provider_finished".']); @@ -205,7 +181,7 @@ class JobStatusController extends Controller $this->repository->setStatus($importJob, 'storing_data'); try { - $this->importFromJob($importJob); + $this->storeTransactions($importJob); } catch (FireflyException $e) { $message = 'The import storage routine crashed: ' . $e->getMessage(); Log::error($message); @@ -216,9 +192,9 @@ class JobStatusController extends Controller return response()->json(['status' => 'NOK', 'message' => $message]); } + // set storage to be finished: + $this->repository->setStatus($importJob, 'storage_finished'); - // set job to be finished. - $this->repository->setStatus($importJob, 'finished'); // expect nothing from routine, just return OK to user. return response()->json(['status' => 'OK', 'message' => 'storage_finished']); @@ -229,90 +205,15 @@ class JobStatusController extends Controller * * @throws FireflyException */ - private function importFromJob(ImportJob $importJob): void + private function storeTransactions(ImportJob $importJob): void { + /** @var ImportArrayStorage $storage */ + $storage = app(ImportArrayStorage::class); + $storage->setJob($importJob); try { - $storage = new ImportArrayStorage($importJob); - $journals = $storage->store(); - $extendedStatus = $importJob->extended_status; - $extendedStatus['count'] = $journals->count(); - $this->repository->setExtendedStatus($importJob, $extendedStatus); - } catch (FireflyException|Exception|FatalThrowableError $e) { + $storage->store(); + } catch (FireflyException|Exception $e) { throw new FireflyException($e->getMessage()); } - - } - - // /** - // * @param ImportJob $job - // * - // * @return \Illuminate\Contracts\View\Factory|\Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector|\Illuminate\View\View - // */ - // public function index(ImportJob $job) - // { - // $statuses = ['configured', 'running', 'finished', 'error']; - // if (!\in_array($job->status, $statuses)) { - // return redirect(route('import.configure', [$job->key])); - // } - // $subTitle = trans('import.status_sub_title'); - // $subTitleIcon = 'fa-star'; - // - // return view('import.status', compact('job', 'subTitle', 'subTitleIcon')); - // } - // - // /** - // * Show status of import job in JSON. - // * - // * @param ImportJob $job - // * - // * @return \Illuminate\Http\JsonResponse - // */ - // public function json(ImportJob $job) - // { - // $result = [ - // 'started' => false, - // 'finished' => false, - // 'running' => false, - // 'errors' => array_values($job->extended_status['errors']), - // 'percentage' => 0, - // 'show_percentage' => false, - // 'steps' => $job->extended_status['steps'], - // 'done' => $job->extended_status['done'], - // 'statusText' => trans('import.status_job_' . $job->status), - // 'status' => $job->status, - // 'finishedText' => '', - // ]; - // - // if (0 !== $job->extended_status['steps']) { - // $result['percentage'] = round(($job->extended_status['done'] / $job->extended_status['steps']) * 100, 0); - // $result['show_percentage'] = true; - // } - // if ('finished' === $job->status) { - // $result['finished'] = true; - // $tagId = (int)$job->extended_status['tag']; - // if ($tagId !== 0) { - // /** @var TagRepositoryInterface $repository */ - // $repository = app(TagRepositoryInterface::class); - // $tag = $repository->find($tagId); - // $count = $tag->transactionJournals()->count(); - // $result['finishedText'] = trans( - // 'import.status_finished_job', ['count' => $count, 'link' => route('tags.show', [$tag->id, 'all']), 'tag' => $tag->tag] - // ); - // } - // - // if ($tagId === 0) { - // $result['finishedText'] = trans('import.status_finished_no_tag'); // @codeCoverageIgnore - // } - // } - // - // if ('running' === $job->status) { - // $result['started'] = true; - // $result['running'] = true; - // } - // $result['percentage'] = $result['percentage'] > 100 ? 100 : $result['percentage']; - // Log::debug(sprintf('JOB STATUS: %d/%d', $result['done'], $result['steps'])); - // - // return response()->json($result); - // } } diff --git a/app/Http/Controllers/Import/PrerequisitesController.php b/app/Http/Controllers/Import/PrerequisitesController.php index e7d52de775..6d7fc3a25a 100644 --- a/app/Http/Controllers/Import/PrerequisitesController.php +++ b/app/Http/Controllers/Import/PrerequisitesController.php @@ -51,7 +51,6 @@ class PrerequisitesController extends Controller function ($request, $next) { app('view')->share('mainTitleIcon', 'fa-archive'); app('view')->share('title', trans('firefly.import_index_title')); - app('view')->share('subTitleIcon', 'fa-check'); $this->repository = app(ImportJobRepositoryInterface::class); @@ -59,7 +58,6 @@ class PrerequisitesController extends Controller return $next($request); } ); - $this->middleware(IsDemoUser::class); } /** diff --git a/app/Import/Routine/BunqRoutine.php b/app/Import/Routine/BunqRoutine.php index 98156f9d7e..ff4b79112e 100644 --- a/app/Import/Routine/BunqRoutine.php +++ b/app/Import/Routine/BunqRoutine.php @@ -85,830 +85,854 @@ use Preferences; */ class BunqRoutine implements RoutineInterface { - /** @var Collection */ - public $errors; - /** @var Collection */ - public $journals; - /** @var int */ - public $lines = 0; - /** @var AccountFactory */ - private $accountFactory; - /** @var AccountRepositoryInterface */ - private $accountRepository; - /** @var ImportJob */ - private $job; - /** @var TransactionJournalFactory */ - private $journalFactory; - /** @var ImportJobRepositoryInterface */ - private $repository; - +// /** @var Collection */ +// public $errors; +// /** @var Collection */ +// public $journals; +// /** @var int */ +// public $lines = 0; +// /** @var AccountFactory */ +// private $accountFactory; +// /** @var AccountRepositoryInterface */ +// private $accountRepository; +// /** @var ImportJob */ +// private $job; +// /** @var TransactionJournalFactory */ +// private $journalFactory; +// /** @var ImportJobRepositoryInterface */ +// private $repository; +// +// /** +// * ImportRoutine constructor. +// */ +// public function __construct() +// { +// $this->journals = new Collection; +// $this->errors = new Collection; +// } +// +// /** +// * @return Collection +// */ +// public function getErrors(): Collection +// { +// return $this->errors; +// } +// +// /** +// * @return Collection +// */ +// public function getJournals(): Collection +// { +// return $this->journals; +// } +// +// /** +// * @return int +// */ +// public function getLines(): int +// { +// return $this->lines; +// } +// +// /** +// * @return bool +// * +// * @throws FireflyException +// */ +// public function run(): bool +// { +// Log::info(sprintf('Start with import job %s using Bunq.', $this->job->key)); +// set_time_limit(0); +// // this method continues with the job and is called by whenever a stage is +// // finished +// $this->continueJob(); +// +// return true; +// } +// +// /** +// * @param ImportJob $job +// */ +// public function setJob(ImportJob $job) +// { +// $this->job = $job; +// $this->repository = app(ImportJobRepositoryInterface::class); +// $this->accountRepository = app(AccountRepositoryInterface::class); +// $this->accountFactory = app(AccountFactory::class); +// $this->journalFactory = app(TransactionJournalFactory::class); +// $this->repository->setUser($job->user); +// $this->accountRepository->setUser($job->user); +// $this->accountFactory->setUser($job->user); +// $this->journalFactory->setUser($job->user); +// } +// +// /** +// * @throws FireflyException +// */ +// protected function continueJob() +// { +// // if in "configuring" +// if ('configuring' === $this->getStatus()) { +// Log::debug('Job is in configuring stage, will do nothing.'); +// +// return; +// } +// $stage = $this->getConfig()['stage'] ?? 'unknown'; +// Log::debug(sprintf('Now in continueJob() for stage %s', $stage)); +// switch ($stage) { +// case 'initial': +// // register device and get tokens. +// $this->runStageInitial(); +// $this->continueJob(); +// break; +// case 'registered': +// // get all bank accounts of user. +// $this->runStageRegistered(); +// $this->continueJob(); +// break; +// case 'logged-in': +// $this->runStageLoggedIn(); +// break; +// case 'have-accounts': +// // do nothing in this stage. Job should revert to config routine. +// break; +// case 'have-account-mapping': +// $this->setStatus('running'); +// $this->runStageHaveAccountMapping(); +// +// break; +// default: +// throw new FireflyException(sprintf('No action for stage %s!', $stage)); +// break; +// } +// } +// +// /** +// * @throws FireflyException +// */ +// protected function runStageInitial(): void +// { +// $this->addStep(); +// Log::debug('In runStageInitial()'); +// $this->setStatus('running'); +// +// // register the device at Bunq: +// $serverId = $this->registerDevice(); +// Log::debug(sprintf('Found device server with id %d', $serverId->getId())); +// +// $config = $this->getConfig(); +// $config['stage'] = 'registered'; +// $this->setConfig($config); +// $this->addStep(); +// } +// +// /** +// * Get a session token + userperson + usercompany. Store it in the job. +// * +// * @throws FireflyException +// */ +// protected function runStageRegistered(): void +// { +// $this->addStep(); +// Log::debug('Now in runStageRegistered()'); +// $apiKey = (string)Preferences::getForUser($this->job->user, 'bunq_api_key')->data; +// $serverPublicKey = new ServerPublicKey(Preferences::getForUser($this->job->user, 'bunq_server_public_key', [])->data); +// $installationToken = $this->getInstallationToken(); +// $request = new DeviceSessionRequest; +// $request->setInstallationToken($installationToken); +// $request->setPrivateKey($this->getPrivateKey()); +// $request->setServerPublicKey($serverPublicKey); +// $request->setSecret($apiKey); +// $request->call(); +// $this->addStep(); +// +// Log::debug('Requested new session.'); +// +// $deviceSession = $request->getDeviceSessionId(); +// $userPerson = $request->getUserPerson(); +// $userCompany = $request->getUserCompany(); +// $sessionToken = $request->getSessionToken(); +// +// $config = $this->getConfig(); +// $config['device_session_id'] = $deviceSession->toArray(); +// $config['user_person'] = $userPerson->toArray(); +// $config['user_company'] = $userCompany->toArray(); +// $config['session_token'] = $sessionToken->toArray(); +// $config['stage'] = 'logged-in'; +// $this->setConfig($config); +// $this->addStep(); +// +// Log::debug('Session stored in job.'); +// } +// +// /** +// * Shorthand method. +// */ +// private function addStep(): void +// { +// $this->addSteps(1); +// } +// +// /** +// * Shorthand method. +// * +// * @param int $count +// */ +// private function addSteps(int $count): void +// { +// $this->repository->addStepsDone($this->job, $count); +// } +// +// /** +// * Shorthand method +// * +// * @param int $steps +// */ +// private function addTotalSteps(int $steps): void +// { +// $this->repository->addTotalSteps($this->job, $steps); +// } +// +// /** +// * @param int $paymentId +// * +// * @return bool +// */ +// private function alreadyImported(int $paymentId): bool +// { +// $count = TransactionJournalMeta::where('name', 'bunq_payment_id') +// ->where('data', json_encode($paymentId))->count(); +// +// Log::debug(sprintf('Transaction #%d is %d time(s) in the database.', $paymentId, $count)); +// +// return $count > 0; +// } +// +// /** +// * @param LabelMonetaryAccount $party +// * @param string $expectedType +// * +// * @return Account +// */ +// private function convertToAccount(LabelMonetaryAccount $party, string $expectedType): Account +// { +// Log::debug('in convertToAccount()'); +// +// if ($party->getIban() !== null) { +// // find opposing party by IBAN first. +// $result = $this->accountRepository->findByIbanNull($party->getIban(), [$expectedType]); +// if (null !== $result) { +// Log::debug(sprintf('Search for %s resulted in account %s (#%d)', $party->getIban(), $result->name, $result->id)); +// +// return $result; +// } +// +// // try to find asset account just in case: +// if ($expectedType !== AccountType::ASSET) { +// $result = $this->accountRepository->findByIbanNull($party->getIban(), [AccountType::ASSET]); +// if (null !== $result) { +// Log::debug(sprintf('Search for Asset "%s" resulted in account %s (#%d)', $party->getIban(), $result->name, $result->id)); +// +// return $result; +// } +// } +// } +// +// // create new account: +// $data = [ +// 'user_id' => $this->job->user_id, +// 'iban' => $party->getIban(), +// 'name' => $party->getLabelUser()->getDisplayName(), +// 'account_type_id' => null, +// 'accountType' => $expectedType, +// 'virtualBalance' => null, +// 'active' => true, +// +// ]; +// $account = $this->accountFactory->create($data); +// Log::debug( +// sprintf( +// 'Converted label monetary account %s to %s account %s (#%d)', +// $party->getLabelUser()->getDisplayName(), +// $expectedType, +// $account->name, $account->id +// ) +// ); +// +// return $account; +// } +// +// /** +// * This method creates a new public/private keypair for the user. This isn't really secure, since the key is generated on the fly with +// * no regards for HSM's, smart cards or other things. It would require some low level programming to get this right. But the private key +// * is stored encrypted in the database so it's something. +// */ +// private function createKeyPair(): void +// { +// Log::debug('Now in createKeyPair()'); +// $private = Preferences::getForUser($this->job->user, 'bunq_private_key', null); +// $public = Preferences::getForUser($this->job->user, 'bunq_public_key', null); +// +// if (!(null === $private && null === $public)) { +// Log::info('Already have public and private key, return NULL.'); +// +// return; +// } +// +// Log::debug('Generate new key pair for user.'); +// $keyConfig = [ +// 'digest_alg' => 'sha512', +// 'private_key_bits' => 2048, +// 'private_key_type' => OPENSSL_KEYTYPE_RSA, +// ]; +// // Create the private and public key +// $res = openssl_pkey_new($keyConfig); +// +// // Extract the private key from $res to $privKey +// $privKey = ''; +// openssl_pkey_export($res, $privKey); +// +// // Extract the public key from $res to $pubKey +// $pubKey = openssl_pkey_get_details($res); +// +// Preferences::setForUser($this->job->user, 'bunq_private_key', $privKey); +// Preferences::setForUser($this->job->user, 'bunq_public_key', $pubKey['key']); +// Log::debug('Created and stored key pair'); +// } +// +// /** +// * Shorthand method. +// * +// * @return array +// */ +// private function getConfig(): array +// { +// return $this->repository->getConfiguration($this->job); +// } +// +// /** +// * Try to detect the current device ID (in case this instance has been registered already. +// * +// * @return DeviceServerId +// * +// * @throws FireflyException +// */ +// private function getExistingDevice(): ?DeviceServerId +// { +// Log::debug('Now in getExistingDevice()'); +// $installationToken = $this->getInstallationToken(); +// $serverPublicKey = $this->getServerPublicKey(); +// $request = new ListDeviceServerRequest; +// $remoteIp = $this->getRemoteIp(); +// $request->setInstallationToken($installationToken); +// $request->setServerPublicKey($serverPublicKey); +// $request->setPrivateKey($this->getPrivateKey()); +// $request->call(); +// $devices = $request->getDevices(); +// /** @var DeviceServer $device */ +// foreach ($devices as $device) { +// if ($device->getIp() === $remoteIp) { +// Log::debug(sprintf('This instance is registered as device #%s', $device->getId()->getId())); +// +// return $device->getId(); +// } +// } +// Log::info('This instance is not yet registered.'); +// +// return null; +// } +// +// /** +// * Shorthand method. +// * +// * @return array +// */ +// private function getExtendedStatus(): array +// { +// return $this->repository->getExtendedStatus($this->job); +// } +// +// /** +// * Get the installation token, either from the users preferences or from Bunq. +// * +// * @return InstallationToken +// * +// * @throws FireflyException +// */ +// private function getInstallationToken(): InstallationToken +// { +// Log::debug('Now in getInstallationToken().'); +// $token = Preferences::getForUser($this->job->user, 'bunq_installation_token', null); +// if (null !== $token) { +// Log::debug('Have installation token, return it.'); +// +// return new InstallationToken($token->data); +// } +// Log::debug('Have no installation token, request one.'); +// +// // verify bunq api code: +// $publicKey = $this->getPublicKey(); +// $request = new InstallationTokenRequest; +// $request->setPublicKey($publicKey); +// $request->call(); +// Log::debug('Sent request for installation token.'); +// +// $installationToken = $request->getInstallationToken(); +// $installationId = $request->getInstallationId(); +// $serverPublicKey = $request->getServerPublicKey(); +// +// Log::debug('Have all values from InstallationTokenRequest'); +// +// +// Preferences::setForUser($this->job->user, 'bunq_installation_token', $installationToken->toArray()); +// Preferences::setForUser($this->job->user, 'bunq_installation_id', $installationId->toArray()); +// Preferences::setForUser($this->job->user, 'bunq_server_public_key', $serverPublicKey->toArray()); +// +// Log::debug('Stored token, ID and pub key.'); +// +// return $installationToken; +// } +// +// /** +// * Get the private key from the users preferences. +// * +// * @return string +// */ +// private function getPrivateKey(): string +// { +// Log::debug('In getPrivateKey()'); +// $preference = Preferences::getForUser($this->job->user, 'bunq_private_key', null); +// if (null === $preference) { +// Log::debug('private key is null'); +// // create key pair +// $this->createKeyPair(); +// } +// $preference = Preferences::getForUser($this->job->user, 'bunq_private_key', null); +// Log::debug('Return private key for user'); +// +// return (string)$preference->data; +// } +// +// /** +// * Get a public key from the users preferences. +// * +// * @return string +// */ +// private function getPublicKey(): string +// { +// Log::debug('Now in getPublicKey()'); +// $preference = Preferences::getForUser($this->job->user, 'bunq_public_key', null); +// if (null === $preference) { +// Log::debug('public key is NULL.'); +// // create key pair +// $this->createKeyPair(); +// } +// $preference = Preferences::getForUser($this->job->user, 'bunq_public_key', null); +// Log::debug('Return public key for user'); +// +// return (string)$preference->data; +// } +// +// /** +// * Request users server remote IP. Let's assume this value will not change any time soon. +// * +// * @return string +// * +// */ +// private function getRemoteIp(): ?string +// { +// +// $preference = Preferences::getForUser($this->job->user, 'external_ip', null); +// if (null === $preference) { +// +// /** @var IPRetrievalInterface $service */ +// $service = app(IPRetrievalInterface::class); +// $serverIp = $service->getIP(); +// if (null !== $serverIp) { +// Preferences::setForUser($this->job->user, 'external_ip', $serverIp); +// } +// +// return $serverIp; +// } +// +// return $preference->data; +// } +// +// /** +// * Get the public key of the server, necessary to verify server signature. +// * +// * @return ServerPublicKey +// * +// * @throws FireflyException +// */ +// private function getServerPublicKey(): ServerPublicKey +// { +// $pref = Preferences::getForUser($this->job->user, 'bunq_server_public_key', null)->data; +// if (null === $pref) { +// throw new FireflyException('Cannot determine bunq server public key, but should have it at this point.'); +// } +// +// return new ServerPublicKey($pref); +// } +// +// /** +// * Shorthand method. +// * +// * @return string +// */ +// private function getStatus(): string +// { +// return $this->repository->getStatus($this->job); +// } +// +// /** +// * Import the transactions that were found. +// * +// * @param array $payments +// * +// * @throws FireflyException +// */ +// private function importPayments(array $payments): void +// { +// Log::debug('Going to run importPayments()'); +// $journals = new Collection; +// $config = $this->getConfig(); +// foreach ($payments as $accountId => $data) { +// Log::debug(sprintf('Now running for bunq account #%d with %d payment(s).', $accountId, \count($data['payments']))); +// /** @var Payment $payment */ +// foreach ($data['payments'] as $index => $payment) { +// Log::debug(sprintf('Now at payment #%d with ID #%d', $index, $payment->getId())); +// // store or find counter party: +// $counterParty = $payment->getCounterParty(); +// $amount = $payment->getAmount(); +// $paymentId = $payment->getId(); +// if ($this->alreadyImported($paymentId)) { +// Log::error(sprintf('Already imported bunq payment with id #%d', $paymentId)); +// +// // add three steps to keep up +// $this->addSteps(3); +// continue; +// } +// Log::debug(sprintf('Amount is %s %s', $amount->getCurrency(), $amount->getValue())); +// $expected = AccountType::EXPENSE; +// if (bccomp($amount->getValue(), '0') === 1) { +// // amount + means that its a deposit. +// $expected = AccountType::REVENUE; +// Log::debug('Will make opposing account revenue.'); +// } +// $opposing = $this->convertToAccount($counterParty, $expected); +// $account = $this->accountRepository->findNull($config['accounts-mapped'][$accountId]); +// $type = TransactionType::WITHDRAWAL; +// +// $this->addStep(); +// +// Log::debug(sprintf('Will store withdrawal between "%s" (%d) and "%s" (%d)', $account->name, $account->id, $opposing->name, $opposing->id)); +// +// // start storing stuff: +// $source = $account; +// $destination = $opposing; +// if (bccomp($amount->getValue(), '0') === 1) { +// // its a deposit: +// $source = $opposing; +// $destination = $account; +// $type = TransactionType::DEPOSIT; +// Log::debug('Will make it a deposit.'); +// } +// if ($account->accountType->type === AccountType::ASSET && $opposing->accountType->type === AccountType::ASSET) { +// $type = TransactionType::TRANSFER; +// Log::debug('Both are assets, will make transfer.'); +// } +// +// $storeData = [ +// 'user' => $this->job->user_id, +// 'type' => $type, +// 'date' => $payment->getCreated(), +// 'description' => $payment->getDescription(), +// 'piggy_bank_id' => null, +// 'piggy_bank_name' => null, +// 'bill_id' => null, +// 'bill_name' => null, +// 'tags' => [$payment->getType(), $payment->getSubType()], +// 'internal_reference' => $payment->getId(), +// 'notes' => null, +// 'bunq_payment_id' => $payment->getId(), +// 'transactions' => [ +// // single transaction: +// [ +// 'description' => null, +// 'amount' => $amount->getValue(), +// 'currency_id' => null, +// 'currency_code' => $amount->getCurrency(), +// 'foreign_amount' => null, +// 'foreign_currency_id' => null, +// 'foreign_currency_code' => null, +// 'budget_id' => null, +// 'budget_name' => null, +// 'category_id' => null, +// 'category_name' => null, +// 'source_id' => $source->id, +// 'source_name' => null, +// 'destination_id' => $destination->id, +// 'destination_name' => null, +// 'reconciled' => false, +// 'identifier' => 0, +// ], +// ], +// ]; +// $journal = $this->journalFactory->create($storeData); +// Log::debug(sprintf('Stored journal with ID #%d', $journal->id)); +// $this->addStep(); +// $journals->push($journal); +// +// } +// } +// if ($journals->count() > 0) { +// // link to tag +// /** @var TagRepositoryInterface $repository */ +// $repository = app(TagRepositoryInterface::class); +// $repository->setUser($this->job->user); +// $data = [ +// 'tag' => trans('import.import_with_key', ['key' => $this->job->key]), +// 'date' => new Carbon, +// 'description' => null, +// 'latitude' => null, +// 'longitude' => null, +// 'zoomLevel' => null, +// 'tagMode' => 'nothing', +// ]; +// $tag = $repository->store($data); +// $extended = $this->getExtendedStatus(); +// $extended['tag'] = $tag->id; +// $this->setExtendedStatus($extended); +// +// Log::debug(sprintf('Created tag #%d ("%s")', $tag->id, $tag->tag)); +// Log::debug('Looping journals...'); +// $tagId = $tag->id; +// +// foreach ($journals as $journal) { +// Log::debug(sprintf('Linking journal #%d to tag #%d...', $journal->id, $tagId)); +// DB::table('tag_transaction_journal')->insert(['transaction_journal_id' => $journal->id, 'tag_id' => $tagId]); +// $this->addStep(); +// } +// Log::info(sprintf('Linked %d journals to tag #%d ("%s")', $journals->count(), $tag->id, $tag->tag)); +// } +// +// // set status to "finished"? +// // update job: +// $this->setStatus('finished'); +// } +// +// /** +// * To install Firefly III as a new device: +// * - Send an installation token request. +// * - Use this token to send a device server request +// * - Store the installation token +// * - Use the installation token each time we need a session. +// * +// * @throws FireflyException +// */ +// private function registerDevice(): DeviceServerId +// { +// Log::debug('Now in registerDevice()'); +// $deviceServerId = Preferences::getForUser($this->job->user, 'bunq_device_server_id', null); +// $serverIp = $this->getRemoteIp(); +// if (null !== $deviceServerId) { +// Log::debug('Already have device server ID.'); +// +// return new DeviceServerId($deviceServerId->data); +// } +// +// Log::debug('Device server ID is null, we have to find an existing one or register a new one.'); +// $installationToken = $this->getInstallationToken(); +// $serverPublicKey = $this->getServerPublicKey(); +// $apiKey = Preferences::getForUser($this->job->user, 'bunq_api_key', ''); +// $this->addStep(); +// +// // try get the current from a list: +// $deviceServerId = $this->getExistingDevice(); +// $this->addStep(); +// if (null !== $deviceServerId) { +// Log::debug('Found device server ID in existing devices list.'); +// +// return $deviceServerId; +// } +// +// Log::debug('Going to create new DeviceServerRequest() because nothing found in existing list.'); +// $request = new DeviceServerRequest; +// $request->setPrivateKey($this->getPrivateKey()); +// $request->setDescription('Firefly III v' . config('firefly.version') . ' for ' . $this->job->user->email); +// $request->setSecret($apiKey->data); +// $request->setPermittedIps([$serverIp]); +// $request->setInstallationToken($installationToken); +// $request->setServerPublicKey($serverPublicKey); +// $deviceServerId = null; +// // try to register device: +// try { +// $request->call(); +// $deviceServerId = $request->getDeviceServerId(); +// } catch (FireflyException $e) { +// Log::error($e->getMessage()); +// // we really have to quit at this point :( +// throw new FireflyException($e->getMessage()); +// } +// if (null === $deviceServerId) { +// throw new FireflyException('Was not able to register server with bunq. Please see the log files.'); +// } +// +// Preferences::setForUser($this->job->user, 'bunq_device_server_id', $deviceServerId->toArray()); +// Log::debug(sprintf('Server ID: %s', json_encode($deviceServerId))); +// +// return $deviceServerId; +// } +// +// /** +// * Will download the transactions for each account that is selected to be imported from. +// * Will of course also update the number of steps and what-not. +// * +// * @throws FireflyException +// */ +// private function runStageHaveAccountMapping(): void +// { +// $config = $this->getConfig(); +// $user = new UserPerson($config['user_person']); +// $mapping = $config['accounts-mapped']; +// $token = new SessionToken($config['session_token']); +// $count = 0; +// $all = []; +// if (0 === $user->getId()) { +// $user = new UserCompany($config['user_company']); +// Log::debug(sprintf('Will try to get transactions for company #%d', $user->getId())); +// } +// +// $this->addTotalSteps(\count($config['accounts']) * 2); +// +// foreach ($config['accounts'] as $accountData) { +// $this->addStep(); +// $account = new MonetaryAccountBank($accountData); +// $importId = $account->getId(); +// if (isset($mapping[$importId])) { +// Log::debug(sprintf('Will grab payments for account %s', $account->getDescription())); +// $request = new ListPaymentRequest(); +// $request->setPrivateKey($this->getPrivateKey()); +// $request->setServerPublicKey($this->getServerPublicKey()); +// $request->setSessionToken($token); +// $request->setUserId($user->getId()); +// $request->setAccount($account); +// $request->call(); +// $payments = $request->getPayments(); +// +// // store in array +// $all[$account->getId()] = [ +// 'account' => $account, +// 'import_id' => $importId, +// 'payments' => $payments, +// ]; +// $count += \count($payments); +// } +// Log::debug(sprintf('Total number of payments: %d', $count)); +// $this->addStep(); +// // add steps for import: +// $this->addTotalSteps($count * 3); +// $this->importPayments($all); +// } +// +// // update job to be complete, I think? +// } +// +// /** +// * @throws FireflyException +// */ +// private function runStageLoggedIn(): void +// { +// $this->addStep(); +// // grab new session token: +// $config = $this->getConfig(); +// $token = new SessionToken($config['session_token']); +// $user = new UserPerson($config['user_person']); +// if (0 === $user->getId()) { +// $user = new UserCompany($config['user_company']); +// } +// +// // list accounts request +// $request = new ListMonetaryAccountRequest(); +// $request->setServerPublicKey($this->getServerPublicKey()); +// $request->setPrivateKey($this->getPrivateKey()); +// $request->setUserId($user->getId()); +// $request->setSessionToken($token); +// $request->call(); +// $accounts = $request->getMonetaryAccounts(); +// $arr = []; +// Log::debug(sprintf('Get monetary accounts, found %d accounts.', $accounts->count())); +// $this->addStep(); +// +// /** @var MonetaryAccountBank $account */ +// foreach ($accounts as $account) { +// $arr[] = $account->toArray(); +// } +// +// $config = $this->getConfig(); +// $config['accounts'] = $arr; +// $config['stage'] = 'have-accounts'; +// $this->setConfig($config); +// +// // once the accounts are stored, go to configuring stage: +// // update job, set status to "configuring". +// $this->setStatus('configuring'); +// $this->addStep(); +// } +// +// /** +// * Shorthand. +// * +// * @param array $config +// */ +// private function setConfig(array $config): void +// { +// $this->repository->setConfiguration($this->job, $config); +// } +// +// /** +// * Shorthand method. +// * +// * @param array $extended +// */ +// private function setExtendedStatus(array $extended): void +// { +// $this->repository->setExtendedStatus($this->job, $extended); +// } +// +// /** +// * Shorthand. +// * +// * @param string $status +// */ +// private function setStatus(string $status): void +// { +// $this->repository->setStatus($this->job, $status); +// } /** - * ImportRoutine constructor. - */ - public function __construct() - { - $this->journals = new Collection; - $this->errors = new Collection; - } - - /** - * @return Collection - */ - public function getErrors(): Collection - { - return $this->errors; - } - - /** - * @return Collection - */ - public function getJournals(): Collection - { - return $this->journals; - } - - /** - * @return int - */ - public function getLines(): int - { - return $this->lines; - } - - /** - * @return bool + * At the end of each run(), the import routine must set the job to the expected status. * + * The final status of the routine must be "provider_finished". + * + * @return bool * @throws FireflyException */ - public function run(): bool + public function run(): void { - Log::info(sprintf('Start with import job %s using Bunq.', $this->job->key)); - set_time_limit(0); - // this method continues with the job and is called by whenever a stage is - // finished - $this->continueJob(); - - return true; + // TODO: Implement run() method. + throw new NotImplementedException; } /** * @param ImportJob $job + * + * @return mixed */ public function setJob(ImportJob $job) { - $this->job = $job; - $this->repository = app(ImportJobRepositoryInterface::class); - $this->accountRepository = app(AccountRepositoryInterface::class); - $this->accountFactory = app(AccountFactory::class); - $this->journalFactory = app(TransactionJournalFactory::class); - $this->repository->setUser($job->user); - $this->accountRepository->setUser($job->user); - $this->accountFactory->setUser($job->user); - $this->journalFactory->setUser($job->user); - } - - /** - * @throws FireflyException - */ - protected function continueJob() - { - // if in "configuring" - if ('configuring' === $this->getStatus()) { - Log::debug('Job is in configuring stage, will do nothing.'); - - return; - } - $stage = $this->getConfig()['stage'] ?? 'unknown'; - Log::debug(sprintf('Now in continueJob() for stage %s', $stage)); - switch ($stage) { - case 'initial': - // register device and get tokens. - $this->runStageInitial(); - $this->continueJob(); - break; - case 'registered': - // get all bank accounts of user. - $this->runStageRegistered(); - $this->continueJob(); - break; - case 'logged-in': - $this->runStageLoggedIn(); - break; - case 'have-accounts': - // do nothing in this stage. Job should revert to config routine. - break; - case 'have-account-mapping': - $this->setStatus('running'); - $this->runStageHaveAccountMapping(); - - break; - default: - throw new FireflyException(sprintf('No action for stage %s!', $stage)); - break; - } - } - - /** - * @throws FireflyException - */ - protected function runStageInitial(): void - { - $this->addStep(); - Log::debug('In runStageInitial()'); - $this->setStatus('running'); - - // register the device at Bunq: - $serverId = $this->registerDevice(); - Log::debug(sprintf('Found device server with id %d', $serverId->getId())); - - $config = $this->getConfig(); - $config['stage'] = 'registered'; - $this->setConfig($config); - $this->addStep(); - } - - /** - * Get a session token + userperson + usercompany. Store it in the job. - * - * @throws FireflyException - */ - protected function runStageRegistered(): void - { - $this->addStep(); - Log::debug('Now in runStageRegistered()'); - $apiKey = (string)Preferences::getForUser($this->job->user, 'bunq_api_key')->data; - $serverPublicKey = new ServerPublicKey(Preferences::getForUser($this->job->user, 'bunq_server_public_key', [])->data); - $installationToken = $this->getInstallationToken(); - $request = new DeviceSessionRequest; - $request->setInstallationToken($installationToken); - $request->setPrivateKey($this->getPrivateKey()); - $request->setServerPublicKey($serverPublicKey); - $request->setSecret($apiKey); - $request->call(); - $this->addStep(); - - Log::debug('Requested new session.'); - - $deviceSession = $request->getDeviceSessionId(); - $userPerson = $request->getUserPerson(); - $userCompany = $request->getUserCompany(); - $sessionToken = $request->getSessionToken(); - - $config = $this->getConfig(); - $config['device_session_id'] = $deviceSession->toArray(); - $config['user_person'] = $userPerson->toArray(); - $config['user_company'] = $userCompany->toArray(); - $config['session_token'] = $sessionToken->toArray(); - $config['stage'] = 'logged-in'; - $this->setConfig($config); - $this->addStep(); - - Log::debug('Session stored in job.'); - } - - /** - * Shorthand method. - */ - private function addStep(): void - { - $this->addSteps(1); - } - - /** - * Shorthand method. - * - * @param int $count - */ - private function addSteps(int $count): void - { - $this->repository->addStepsDone($this->job, $count); - } - - /** - * Shorthand method - * - * @param int $steps - */ - private function addTotalSteps(int $steps): void - { - $this->repository->addTotalSteps($this->job, $steps); - } - - /** - * @param int $paymentId - * - * @return bool - */ - private function alreadyImported(int $paymentId): bool - { - $count = TransactionJournalMeta::where('name', 'bunq_payment_id') - ->where('data', json_encode($paymentId))->count(); - - Log::debug(sprintf('Transaction #%d is %d time(s) in the database.', $paymentId, $count)); - - return $count > 0; - } - - /** - * @param LabelMonetaryAccount $party - * @param string $expectedType - * - * @return Account - */ - private function convertToAccount(LabelMonetaryAccount $party, string $expectedType): Account - { - Log::debug('in convertToAccount()'); - - if ($party->getIban() !== null) { - // find opposing party by IBAN first. - $result = $this->accountRepository->findByIbanNull($party->getIban(), [$expectedType]); - if (null !== $result) { - Log::debug(sprintf('Search for %s resulted in account %s (#%d)', $party->getIban(), $result->name, $result->id)); - - return $result; - } - - // try to find asset account just in case: - if ($expectedType !== AccountType::ASSET) { - $result = $this->accountRepository->findByIbanNull($party->getIban(), [AccountType::ASSET]); - if (null !== $result) { - Log::debug(sprintf('Search for Asset "%s" resulted in account %s (#%d)', $party->getIban(), $result->name, $result->id)); - - return $result; - } - } - } - - // create new account: - $data = [ - 'user_id' => $this->job->user_id, - 'iban' => $party->getIban(), - 'name' => $party->getLabelUser()->getDisplayName(), - 'account_type_id' => null, - 'accountType' => $expectedType, - 'virtualBalance' => null, - 'active' => true, - - ]; - $account = $this->accountFactory->create($data); - Log::debug( - sprintf( - 'Converted label monetary account %s to %s account %s (#%d)', - $party->getLabelUser()->getDisplayName(), - $expectedType, - $account->name, $account->id - ) - ); - - return $account; - } - - /** - * This method creates a new public/private keypair for the user. This isn't really secure, since the key is generated on the fly with - * no regards for HSM's, smart cards or other things. It would require some low level programming to get this right. But the private key - * is stored encrypted in the database so it's something. - */ - private function createKeyPair(): void - { - Log::debug('Now in createKeyPair()'); - $private = Preferences::getForUser($this->job->user, 'bunq_private_key', null); - $public = Preferences::getForUser($this->job->user, 'bunq_public_key', null); - - if (!(null === $private && null === $public)) { - Log::info('Already have public and private key, return NULL.'); - - return; - } - - Log::debug('Generate new key pair for user.'); - $keyConfig = [ - 'digest_alg' => 'sha512', - 'private_key_bits' => 2048, - 'private_key_type' => OPENSSL_KEYTYPE_RSA, - ]; - // Create the private and public key - $res = openssl_pkey_new($keyConfig); - - // Extract the private key from $res to $privKey - $privKey = ''; - openssl_pkey_export($res, $privKey); - - // Extract the public key from $res to $pubKey - $pubKey = openssl_pkey_get_details($res); - - Preferences::setForUser($this->job->user, 'bunq_private_key', $privKey); - Preferences::setForUser($this->job->user, 'bunq_public_key', $pubKey['key']); - Log::debug('Created and stored key pair'); - } - - /** - * Shorthand method. - * - * @return array - */ - private function getConfig(): array - { - return $this->repository->getConfiguration($this->job); - } - - /** - * Try to detect the current device ID (in case this instance has been registered already. - * - * @return DeviceServerId - * - * @throws FireflyException - */ - private function getExistingDevice(): ?DeviceServerId - { - Log::debug('Now in getExistingDevice()'); - $installationToken = $this->getInstallationToken(); - $serverPublicKey = $this->getServerPublicKey(); - $request = new ListDeviceServerRequest; - $remoteIp = $this->getRemoteIp(); - $request->setInstallationToken($installationToken); - $request->setServerPublicKey($serverPublicKey); - $request->setPrivateKey($this->getPrivateKey()); - $request->call(); - $devices = $request->getDevices(); - /** @var DeviceServer $device */ - foreach ($devices as $device) { - if ($device->getIp() === $remoteIp) { - Log::debug(sprintf('This instance is registered as device #%s', $device->getId()->getId())); - - return $device->getId(); - } - } - Log::info('This instance is not yet registered.'); - - return null; - } - - /** - * Shorthand method. - * - * @return array - */ - private function getExtendedStatus(): array - { - return $this->repository->getExtendedStatus($this->job); - } - - /** - * Get the installation token, either from the users preferences or from Bunq. - * - * @return InstallationToken - * - * @throws FireflyException - */ - private function getInstallationToken(): InstallationToken - { - Log::debug('Now in getInstallationToken().'); - $token = Preferences::getForUser($this->job->user, 'bunq_installation_token', null); - if (null !== $token) { - Log::debug('Have installation token, return it.'); - - return new InstallationToken($token->data); - } - Log::debug('Have no installation token, request one.'); - - // verify bunq api code: - $publicKey = $this->getPublicKey(); - $request = new InstallationTokenRequest; - $request->setPublicKey($publicKey); - $request->call(); - Log::debug('Sent request for installation token.'); - - $installationToken = $request->getInstallationToken(); - $installationId = $request->getInstallationId(); - $serverPublicKey = $request->getServerPublicKey(); - - Log::debug('Have all values from InstallationTokenRequest'); - - - Preferences::setForUser($this->job->user, 'bunq_installation_token', $installationToken->toArray()); - Preferences::setForUser($this->job->user, 'bunq_installation_id', $installationId->toArray()); - Preferences::setForUser($this->job->user, 'bunq_server_public_key', $serverPublicKey->toArray()); - - Log::debug('Stored token, ID and pub key.'); - - return $installationToken; - } - - /** - * Get the private key from the users preferences. - * - * @return string - */ - private function getPrivateKey(): string - { - Log::debug('In getPrivateKey()'); - $preference = Preferences::getForUser($this->job->user, 'bunq_private_key', null); - if (null === $preference) { - Log::debug('private key is null'); - // create key pair - $this->createKeyPair(); - } - $preference = Preferences::getForUser($this->job->user, 'bunq_private_key', null); - Log::debug('Return private key for user'); - - return (string)$preference->data; - } - - /** - * Get a public key from the users preferences. - * - * @return string - */ - private function getPublicKey(): string - { - Log::debug('Now in getPublicKey()'); - $preference = Preferences::getForUser($this->job->user, 'bunq_public_key', null); - if (null === $preference) { - Log::debug('public key is NULL.'); - // create key pair - $this->createKeyPair(); - } - $preference = Preferences::getForUser($this->job->user, 'bunq_public_key', null); - Log::debug('Return public key for user'); - - return (string)$preference->data; - } - - /** - * Request users server remote IP. Let's assume this value will not change any time soon. - * - * @return string - * - */ - private function getRemoteIp(): ?string - { - - $preference = Preferences::getForUser($this->job->user, 'external_ip', null); - if (null === $preference) { - - /** @var IPRetrievalInterface $service */ - $service = app(IPRetrievalInterface::class); - $serverIp = $service->getIP(); - if (null !== $serverIp) { - Preferences::setForUser($this->job->user, 'external_ip', $serverIp); - } - - return $serverIp; - } - - return $preference->data; - } - - /** - * Get the public key of the server, necessary to verify server signature. - * - * @return ServerPublicKey - * - * @throws FireflyException - */ - private function getServerPublicKey(): ServerPublicKey - { - $pref = Preferences::getForUser($this->job->user, 'bunq_server_public_key', null)->data; - if (null === $pref) { - throw new FireflyException('Cannot determine bunq server public key, but should have it at this point.'); - } - - return new ServerPublicKey($pref); - } - - /** - * Shorthand method. - * - * @return string - */ - private function getStatus(): string - { - return $this->repository->getStatus($this->job); - } - - /** - * Import the transactions that were found. - * - * @param array $payments - * - * @throws FireflyException - */ - private function importPayments(array $payments): void - { - Log::debug('Going to run importPayments()'); - $journals = new Collection; - $config = $this->getConfig(); - foreach ($payments as $accountId => $data) { - Log::debug(sprintf('Now running for bunq account #%d with %d payment(s).', $accountId, \count($data['payments']))); - /** @var Payment $payment */ - foreach ($data['payments'] as $index => $payment) { - Log::debug(sprintf('Now at payment #%d with ID #%d', $index, $payment->getId())); - // store or find counter party: - $counterParty = $payment->getCounterParty(); - $amount = $payment->getAmount(); - $paymentId = $payment->getId(); - if ($this->alreadyImported($paymentId)) { - Log::error(sprintf('Already imported bunq payment with id #%d', $paymentId)); - - // add three steps to keep up - $this->addSteps(3); - continue; - } - Log::debug(sprintf('Amount is %s %s', $amount->getCurrency(), $amount->getValue())); - $expected = AccountType::EXPENSE; - if (bccomp($amount->getValue(), '0') === 1) { - // amount + means that its a deposit. - $expected = AccountType::REVENUE; - Log::debug('Will make opposing account revenue.'); - } - $opposing = $this->convertToAccount($counterParty, $expected); - $account = $this->accountRepository->findNull($config['accounts-mapped'][$accountId]); - $type = TransactionType::WITHDRAWAL; - - $this->addStep(); - - Log::debug(sprintf('Will store withdrawal between "%s" (%d) and "%s" (%d)', $account->name, $account->id, $opposing->name, $opposing->id)); - - // start storing stuff: - $source = $account; - $destination = $opposing; - if (bccomp($amount->getValue(), '0') === 1) { - // its a deposit: - $source = $opposing; - $destination = $account; - $type = TransactionType::DEPOSIT; - Log::debug('Will make it a deposit.'); - } - if ($account->accountType->type === AccountType::ASSET && $opposing->accountType->type === AccountType::ASSET) { - $type = TransactionType::TRANSFER; - Log::debug('Both are assets, will make transfer.'); - } - - $storeData = [ - 'user' => $this->job->user_id, - 'type' => $type, - 'date' => $payment->getCreated(), - 'description' => $payment->getDescription(), - 'piggy_bank_id' => null, - 'piggy_bank_name' => null, - 'bill_id' => null, - 'bill_name' => null, - 'tags' => [$payment->getType(), $payment->getSubType()], - 'internal_reference' => $payment->getId(), - 'notes' => null, - 'bunq_payment_id' => $payment->getId(), - 'transactions' => [ - // single transaction: - [ - 'description' => null, - 'amount' => $amount->getValue(), - 'currency_id' => null, - 'currency_code' => $amount->getCurrency(), - 'foreign_amount' => null, - 'foreign_currency_id' => null, - 'foreign_currency_code' => null, - 'budget_id' => null, - 'budget_name' => null, - 'category_id' => null, - 'category_name' => null, - 'source_id' => $source->id, - 'source_name' => null, - 'destination_id' => $destination->id, - 'destination_name' => null, - 'reconciled' => false, - 'identifier' => 0, - ], - ], - ]; - $journal = $this->journalFactory->create($storeData); - Log::debug(sprintf('Stored journal with ID #%d', $journal->id)); - $this->addStep(); - $journals->push($journal); - - } - } - if ($journals->count() > 0) { - // link to tag - /** @var TagRepositoryInterface $repository */ - $repository = app(TagRepositoryInterface::class); - $repository->setUser($this->job->user); - $data = [ - 'tag' => trans('import.import_with_key', ['key' => $this->job->key]), - 'date' => new Carbon, - 'description' => null, - 'latitude' => null, - 'longitude' => null, - 'zoomLevel' => null, - 'tagMode' => 'nothing', - ]; - $tag = $repository->store($data); - $extended = $this->getExtendedStatus(); - $extended['tag'] = $tag->id; - $this->setExtendedStatus($extended); - - Log::debug(sprintf('Created tag #%d ("%s")', $tag->id, $tag->tag)); - Log::debug('Looping journals...'); - $tagId = $tag->id; - - foreach ($journals as $journal) { - Log::debug(sprintf('Linking journal #%d to tag #%d...', $journal->id, $tagId)); - DB::table('tag_transaction_journal')->insert(['transaction_journal_id' => $journal->id, 'tag_id' => $tagId]); - $this->addStep(); - } - Log::info(sprintf('Linked %d journals to tag #%d ("%s")', $journals->count(), $tag->id, $tag->tag)); - } - - // set status to "finished"? - // update job: - $this->setStatus('finished'); - } - - /** - * To install Firefly III as a new device: - * - Send an installation token request. - * - Use this token to send a device server request - * - Store the installation token - * - Use the installation token each time we need a session. - * - * @throws FireflyException - */ - private function registerDevice(): DeviceServerId - { - Log::debug('Now in registerDevice()'); - $deviceServerId = Preferences::getForUser($this->job->user, 'bunq_device_server_id', null); - $serverIp = $this->getRemoteIp(); - if (null !== $deviceServerId) { - Log::debug('Already have device server ID.'); - - return new DeviceServerId($deviceServerId->data); - } - - Log::debug('Device server ID is null, we have to find an existing one or register a new one.'); - $installationToken = $this->getInstallationToken(); - $serverPublicKey = $this->getServerPublicKey(); - $apiKey = Preferences::getForUser($this->job->user, 'bunq_api_key', ''); - $this->addStep(); - - // try get the current from a list: - $deviceServerId = $this->getExistingDevice(); - $this->addStep(); - if (null !== $deviceServerId) { - Log::debug('Found device server ID in existing devices list.'); - - return $deviceServerId; - } - - Log::debug('Going to create new DeviceServerRequest() because nothing found in existing list.'); - $request = new DeviceServerRequest; - $request->setPrivateKey($this->getPrivateKey()); - $request->setDescription('Firefly III v' . config('firefly.version') . ' for ' . $this->job->user->email); - $request->setSecret($apiKey->data); - $request->setPermittedIps([$serverIp]); - $request->setInstallationToken($installationToken); - $request->setServerPublicKey($serverPublicKey); - $deviceServerId = null; - // try to register device: - try { - $request->call(); - $deviceServerId = $request->getDeviceServerId(); - } catch (FireflyException $e) { - Log::error($e->getMessage()); - // we really have to quit at this point :( - throw new FireflyException($e->getMessage()); - } - if (null === $deviceServerId) { - throw new FireflyException('Was not able to register server with bunq. Please see the log files.'); - } - - Preferences::setForUser($this->job->user, 'bunq_device_server_id', $deviceServerId->toArray()); - Log::debug(sprintf('Server ID: %s', json_encode($deviceServerId))); - - return $deviceServerId; - } - - /** - * Will download the transactions for each account that is selected to be imported from. - * Will of course also update the number of steps and what-not. - * - * @throws FireflyException - */ - private function runStageHaveAccountMapping(): void - { - $config = $this->getConfig(); - $user = new UserPerson($config['user_person']); - $mapping = $config['accounts-mapped']; - $token = new SessionToken($config['session_token']); - $count = 0; - $all = []; - if (0 === $user->getId()) { - $user = new UserCompany($config['user_company']); - Log::debug(sprintf('Will try to get transactions for company #%d', $user->getId())); - } - - $this->addTotalSteps(\count($config['accounts']) * 2); - - foreach ($config['accounts'] as $accountData) { - $this->addStep(); - $account = new MonetaryAccountBank($accountData); - $importId = $account->getId(); - if (isset($mapping[$importId])) { - Log::debug(sprintf('Will grab payments for account %s', $account->getDescription())); - $request = new ListPaymentRequest(); - $request->setPrivateKey($this->getPrivateKey()); - $request->setServerPublicKey($this->getServerPublicKey()); - $request->setSessionToken($token); - $request->setUserId($user->getId()); - $request->setAccount($account); - $request->call(); - $payments = $request->getPayments(); - - // store in array - $all[$account->getId()] = [ - 'account' => $account, - 'import_id' => $importId, - 'payments' => $payments, - ]; - $count += \count($payments); - } - Log::debug(sprintf('Total number of payments: %d', $count)); - $this->addStep(); - // add steps for import: - $this->addTotalSteps($count * 3); - $this->importPayments($all); - } - - // update job to be complete, I think? - } - - /** - * @throws FireflyException - */ - private function runStageLoggedIn(): void - { - $this->addStep(); - // grab new session token: - $config = $this->getConfig(); - $token = new SessionToken($config['session_token']); - $user = new UserPerson($config['user_person']); - if (0 === $user->getId()) { - $user = new UserCompany($config['user_company']); - } - - // list accounts request - $request = new ListMonetaryAccountRequest(); - $request->setServerPublicKey($this->getServerPublicKey()); - $request->setPrivateKey($this->getPrivateKey()); - $request->setUserId($user->getId()); - $request->setSessionToken($token); - $request->call(); - $accounts = $request->getMonetaryAccounts(); - $arr = []; - Log::debug(sprintf('Get monetary accounts, found %d accounts.', $accounts->count())); - $this->addStep(); - - /** @var MonetaryAccountBank $account */ - foreach ($accounts as $account) { - $arr[] = $account->toArray(); - } - - $config = $this->getConfig(); - $config['accounts'] = $arr; - $config['stage'] = 'have-accounts'; - $this->setConfig($config); - - // once the accounts are stored, go to configuring stage: - // update job, set status to "configuring". - $this->setStatus('configuring'); - $this->addStep(); - } - - /** - * Shorthand. - * - * @param array $config - */ - private function setConfig(array $config): void - { - $this->repository->setConfiguration($this->job, $config); - } - - /** - * Shorthand method. - * - * @param array $extended - */ - private function setExtendedStatus(array $extended): void - { - $this->repository->setExtendedStatus($this->job, $extended); - } - - /** - * Shorthand. - * - * @param string $status - */ - private function setStatus(string $status): void - { - $this->repository->setStatus($this->job, $status); + // TODO: Implement setJob() method. + throw new NotImplementedException; } } diff --git a/app/Import/Routine/FileRoutine.php b/app/Import/Routine/FileRoutine.php index b4c58bc856..a685eeac1d 100644 --- a/app/Import/Routine/FileRoutine.php +++ b/app/Import/Routine/FileRoutine.php @@ -24,6 +24,7 @@ namespace FireflyIII\Import\Routine; use Carbon\Carbon; use DB; +use FireflyIII\Exceptions\FireflyException; use FireflyIII\Import\FileProcessor\FileProcessorInterface; use FireflyIII\Import\Storage\ImportStorage; use FireflyIII\Models\ImportJob; @@ -38,271 +39,295 @@ use Log; */ class FileRoutine implements RoutineInterface { - /** @var Collection */ - public $errors; - /** @var Collection */ - public $journals; - /** @var int */ - public $lines = 0; - /** @var ImportJob */ - private $job; - - /** @var ImportJobRepositoryInterface */ - private $repository; - - /** - * ImportRoutine constructor. - */ - public function __construct() - { - $this->journals = new Collection; - $this->errors = new Collection; - } - - /** - * @return Collection - */ - public function getErrors(): Collection - { - return $this->errors; - } - - /** - * @return Collection - */ - public function getJournals(): Collection - { - return $this->journals; - } - - /** - * @return int - */ - public function getLines(): int - { - return $this->lines; - } - +// /** @var Collection */ +// public $errors; +// /** @var Collection */ +// public $journals; +// /** @var int */ +// public $lines = 0; +// /** @var ImportJob */ +// private $job; +// +// /** @var ImportJobRepositoryInterface */ +// private $repository; +// +// /** +// * ImportRoutine constructor. +// */ +// public function __construct() +// { +// $this->journals = new Collection; +// $this->errors = new Collection; +// } +// +// /** +// * @return Collection +// */ +// public function getErrors(): Collection +// { +// return $this->errors; +// } +// +// /** +// * @return Collection +// */ +// public function getJournals(): Collection +// { +// return $this->journals; +// } +// +// /** +// * @return int +// */ +// public function getLines(): int +// { +// return $this->lines; +// } +// +// /** +// * +// */ +// public function run(): bool +// { +// if ('configured' !== $this->getStatus()) { +// Log::error(sprintf('Job %s is in state "%s" so it cannot be started.', $this->job->key, $this->getStatus())); +// +// return false; +// } +// set_time_limit(0); +// Log::info(sprintf('Start with import job %s', $this->job->key)); +// +// // total steps: 6 +// $this->setTotalSteps(6); +// +// $importObjects = $this->getImportObjects(); +// $this->lines = $importObjects->count(); +// $this->addStep(); +// +// // total steps can now be extended. File has been scanned. 7 steps per line: +// $this->addTotalSteps(7 * $this->lines); +// +// // once done, use storage thing to actually store them: +// Log::info(sprintf('Returned %d valid objects from file processor', $this->lines)); +// +// $storage = $this->storeObjects($importObjects); +// $this->addStep(); +// Log::debug('Back in run()'); +// +// Log::debug('Updated job...'); +// Log::debug(sprintf('%d journals in $storage->journals', $storage->journals->count())); +// $this->journals = $storage->journals; +// $this->errors = $storage->errors; +// +// Log::debug('Going to call createImportTag()'); +// +// // create tag, link tag to all journals: +// $this->createImportTag(); +// $this->addStep(); +// +// // update job: +// $this->setStatus('finished'); +// +// Log::info(sprintf('Done with import job %s', $this->job->key)); +// +// return true; +// } +// +// /** +// * @param ImportJob $job +// */ +// public function setJob(ImportJob $job) +// { +// $this->job = $job; +// $this->repository = app(ImportJobRepositoryInterface::class); +// $this->repository->setUser($job->user); +// } +// +// /** +// * @return Collection +// */ +// protected function getImportObjects(): Collection +// { +// $objects = new Collection; +// $fileType = $this->getConfig()['file-type'] ?? 'csv'; +// // will only respond to "file" +// $class = config(sprintf('import.options.file.processors.%s', $fileType)); +// /** @var FileProcessorInterface $processor */ +// $processor = app($class); +// $processor->setJob($this->job); +// +// if ('configured' === $this->getStatus()) { +// // set job as "running"... +// $this->setStatus('running'); +// +// Log::debug('Job is configured, start with run()'); +// $processor->run(); +// $objects = $processor->getObjects(); +// } +// +// return $objects; +// } +// +// /** +// * Shorthand method. +// */ +// private function addStep() +// { +// $this->repository->addStepsDone($this->job, 1); +// } +// +// /** +// * Shorthand +// * +// * @param int $steps +// */ +// private function addTotalSteps(int $steps) +// { +// $this->repository->addTotalSteps($this->job, $steps); +// } +// +// /** +// * +// */ +// private function createImportTag(): Tag +// { +// Log::debug('Now in createImportTag()'); +// +// if ($this->journals->count() < 1) { +// Log::info(sprintf('Will not create tag, %d journals imported.', $this->journals->count())); +// +// return new Tag; +// } +// $this->addTotalSteps($this->journals->count() + 2); +// +// /** @var TagRepositoryInterface $repository */ +// $repository = app(TagRepositoryInterface::class); +// $repository->setUser($this->job->user); +// $data = [ +// 'tag' => trans('import.import_with_key', ['key' => $this->job->key]), +// 'date' => new Carbon, +// 'description' => null, +// 'latitude' => null, +// 'longitude' => null, +// 'zoomLevel' => null, +// 'tagMode' => 'nothing', +// ]; +// $tag = $repository->store($data); +// $this->addStep(); +// $extended = $this->getExtendedStatus(); +// $extended['tag'] = $tag->id; +// $this->setExtendedStatus($extended); +// +// Log::debug(sprintf('Created tag #%d ("%s")', $tag->id, $tag->tag)); +// Log::debug('Looping journals...'); +// $journalIds = $this->journals->pluck('id')->toArray(); +// $tagId = $tag->id; +// foreach ($journalIds as $journalId) { +// Log::debug(sprintf('Linking journal #%d to tag #%d...', $journalId, $tagId)); +// DB::table('tag_transaction_journal')->insert(['transaction_journal_id' => $journalId, 'tag_id' => $tagId]); +// $this->addStep(); +// } +// Log::info(sprintf('Linked %d journals to tag #%d ("%s")', $this->journals->count(), $tag->id, $tag->tag)); +// $this->addStep(); +// +// return $tag; +// } +// +// /** +// * Shorthand method +// * +// * @return array +// */ +// private function getConfig(): array +// { +// return $this->repository->getConfiguration($this->job); +// } +// +// /** +// * @return array +// */ +// private function getExtendedStatus(): array +// { +// return $this->repository->getExtendedStatus($this->job); +// } +// +// /** +// * Shorthand method. +// * +// * @return string +// */ +// private function getStatus(): string +// { +// return $this->repository->getStatus($this->job); +// } +// +// /** +// * @param array $extended +// */ +// private function setExtendedStatus(array $extended): void +// { +// $this->repository->setExtendedStatus($this->job, $extended); +// } +// +// /** +// * Shorthand +// * +// * @param string $status +// */ +// private function setStatus(string $status): void +// { +// $this->repository->setStatus($this->job, $status); +// } +// +// /** +// * Shorthand +// * +// * @param int $steps +// */ +// private function setTotalSteps(int $steps) +// { +// $this->repository->setTotalSteps($this->job, $steps); +// } +// +// /** +// * @param Collection $objects +// * +// * @return ImportStorage +// */ +// private function storeObjects(Collection $objects): ImportStorage +// { +// $config = $this->getConfig(); +// $storage = new ImportStorage; +// $storage->setJob($this->job); +// $storage->setDateFormat($config['date-format']); +// $storage->setObjects($objects); +// $storage->store(); +// Log::info('Back in storeObjects()'); +// +// return $storage; +// } /** + * At the end of each run(), the import routine must set the job to the expected status. * + * The final status of the routine must be "provider_finished". + * + * @return bool + * @throws FireflyException */ - public function run(): bool + public function run(): void { - if ('configured' !== $this->getStatus()) { - Log::error(sprintf('Job %s is in state "%s" so it cannot be started.', $this->job->key, $this->getStatus())); - - return false; - } - set_time_limit(0); - Log::info(sprintf('Start with import job %s', $this->job->key)); - - // total steps: 6 - $this->setTotalSteps(6); - - $importObjects = $this->getImportObjects(); - $this->lines = $importObjects->count(); - $this->addStep(); - - // total steps can now be extended. File has been scanned. 7 steps per line: - $this->addTotalSteps(7 * $this->lines); - - // once done, use storage thing to actually store them: - Log::info(sprintf('Returned %d valid objects from file processor', $this->lines)); - - $storage = $this->storeObjects($importObjects); - $this->addStep(); - Log::debug('Back in run()'); - - Log::debug('Updated job...'); - Log::debug(sprintf('%d journals in $storage->journals', $storage->journals->count())); - $this->journals = $storage->journals; - $this->errors = $storage->errors; - - Log::debug('Going to call createImportTag()'); - - // create tag, link tag to all journals: - $this->createImportTag(); - $this->addStep(); - - // update job: - $this->setStatus('finished'); - - Log::info(sprintf('Done with import job %s', $this->job->key)); - - return true; + // TODO: Implement run() method. + throw new NotImplementedException; } /** * @param ImportJob $job + * + * @return mixed */ public function setJob(ImportJob $job) { - $this->job = $job; - $this->repository = app(ImportJobRepositoryInterface::class); - $this->repository->setUser($job->user); - } - - /** - * @return Collection - */ - protected function getImportObjects(): Collection - { - $objects = new Collection; - $fileType = $this->getConfig()['file-type'] ?? 'csv'; - // will only respond to "file" - $class = config(sprintf('import.options.file.processors.%s', $fileType)); - /** @var FileProcessorInterface $processor */ - $processor = app($class); - $processor->setJob($this->job); - - if ('configured' === $this->getStatus()) { - // set job as "running"... - $this->setStatus('running'); - - Log::debug('Job is configured, start with run()'); - $processor->run(); - $objects = $processor->getObjects(); - } - - return $objects; - } - - /** - * Shorthand method. - */ - private function addStep() - { - $this->repository->addStepsDone($this->job, 1); - } - - /** - * Shorthand - * - * @param int $steps - */ - private function addTotalSteps(int $steps) - { - $this->repository->addTotalSteps($this->job, $steps); - } - - /** - * - */ - private function createImportTag(): Tag - { - Log::debug('Now in createImportTag()'); - - if ($this->journals->count() < 1) { - Log::info(sprintf('Will not create tag, %d journals imported.', $this->journals->count())); - - return new Tag; - } - $this->addTotalSteps($this->journals->count() + 2); - - /** @var TagRepositoryInterface $repository */ - $repository = app(TagRepositoryInterface::class); - $repository->setUser($this->job->user); - $data = [ - 'tag' => trans('import.import_with_key', ['key' => $this->job->key]), - 'date' => new Carbon, - 'description' => null, - 'latitude' => null, - 'longitude' => null, - 'zoomLevel' => null, - 'tagMode' => 'nothing', - ]; - $tag = $repository->store($data); - $this->addStep(); - $extended = $this->getExtendedStatus(); - $extended['tag'] = $tag->id; - $this->setExtendedStatus($extended); - - Log::debug(sprintf('Created tag #%d ("%s")', $tag->id, $tag->tag)); - Log::debug('Looping journals...'); - $journalIds = $this->journals->pluck('id')->toArray(); - $tagId = $tag->id; - foreach ($journalIds as $journalId) { - Log::debug(sprintf('Linking journal #%d to tag #%d...', $journalId, $tagId)); - DB::table('tag_transaction_journal')->insert(['transaction_journal_id' => $journalId, 'tag_id' => $tagId]); - $this->addStep(); - } - Log::info(sprintf('Linked %d journals to tag #%d ("%s")', $this->journals->count(), $tag->id, $tag->tag)); - $this->addStep(); - - return $tag; - } - - /** - * Shorthand method - * - * @return array - */ - private function getConfig(): array - { - return $this->repository->getConfiguration($this->job); - } - - /** - * @return array - */ - private function getExtendedStatus(): array - { - return $this->repository->getExtendedStatus($this->job); - } - - /** - * Shorthand method. - * - * @return string - */ - private function getStatus(): string - { - return $this->repository->getStatus($this->job); - } - - /** - * @param array $extended - */ - private function setExtendedStatus(array $extended): void - { - $this->repository->setExtendedStatus($this->job, $extended); - } - - /** - * Shorthand - * - * @param string $status - */ - private function setStatus(string $status): void - { - $this->repository->setStatus($this->job, $status); - } - - /** - * Shorthand - * - * @param int $steps - */ - private function setTotalSteps(int $steps) - { - $this->repository->setTotalSteps($this->job, $steps); - } - - /** - * @param Collection $objects - * - * @return ImportStorage - */ - private function storeObjects(Collection $objects): ImportStorage - { - $config = $this->getConfig(); - $storage = new ImportStorage; - $storage->setJob($this->job); - $storage->setDateFormat($config['date-format']); - $storage->setObjects($objects); - $storage->store(); - Log::info('Back in storeObjects()'); - - return $storage; + // TODO: Implement setJob() method. + throw new NotImplementedException; } } diff --git a/app/Import/Routine/SpectreRoutine.php b/app/Import/Routine/SpectreRoutine.php index 3147f7788f..abde781fad 100644 --- a/app/Import/Routine/SpectreRoutine.php +++ b/app/Import/Routine/SpectreRoutine.php @@ -52,542 +52,566 @@ use Preferences; */ class SpectreRoutine implements RoutineInterface { - /** @var Collection */ - public $errors; - /** @var Collection */ - public $journals; - /** @var int */ - public $lines = 0; - /** @var ImportJob */ - private $job; - - /** @var ImportJobRepositoryInterface */ - private $repository; - +// /** @var Collection */ +// public $errors; +// /** @var Collection */ +// public $journals; +// /** @var int */ +// public $lines = 0; +// /** @var ImportJob */ +// private $job; +// +// /** @var ImportJobRepositoryInterface */ +// private $repository; +// +// /** +// * ImportRoutine constructor. +// */ +// public function __construct() +// { +// $this->journals = new Collection; +// $this->errors = new Collection; +// } +// +// /** +// * @return Collection +// */ +// public function getErrors(): Collection +// { +// return $this->errors; +// } +// +// /** +// * @return Collection +// */ +// public function getJournals(): Collection +// { +// return $this->journals; +// } +// +// /** +// * @return int +// */ +// public function getLines(): int +// { +// return $this->lines; +// } +// +// /** +// * A Spectre job that ends up here is either "configured" or "running", and will be set to "running" +// * when it is "configured". +// * +// * Job has several stages, stored in extended status key 'stage' +// * +// * initial: just begun, nothing happened. action: get a customer and a token. Next status: has-token +// * has-token: redirect user to sandstorm, make user login. set job to: user-logged-in +// * user-logged-in: customer has an attempt. action: analyse/get attempt and go for next status. +// * if attempt failed: job status is error, save a warning somewhere? +// * if success, try to get accounts. Save in config key 'accounts'. set status: have-accounts and "configuring" +// * +// * have-accounts: make user link accounts and select accounts to import from. +// * +// * If job is "configuring" and stage "have-accounts" then present the accounts and make user link them to +// * own asset accounts. Store this mapping, set config to "have-account-mapping" and job status configured". +// * +// * have-account-mapping: start downloading transactions? +// * +// * +// * @return bool +// * +// * @throws FireflyException +// * @throws SpectreException +// * @throws \Illuminate\Container\EntryNotFoundException +// */ +// public function run(): bool +// { +// if ('configured' === $this->getStatus()) { +// $this->repository->updateStatus($this->job, 'running'); +// } +// Log::info(sprintf('Start with import job %s using Spectre.', $this->job->key)); +// set_time_limit(0); +// +// // check if job has token first! +// $stage = $this->getConfig()['stage'] ?? 'unknown'; +// +// switch ($stage) { +// case 'initial': +// // get customer and token: +// $this->runStageInitial(); +// break; +// case 'has-token': +// // import routine does nothing at this point: +// break; +// case 'user-logged-in': +// $this->runStageLoggedIn(); +// break; +// case 'have-account-mapping': +// $this->runStageHaveMapping(); +// break; +// default: +// throw new FireflyException(sprintf('Cannot handle stage %s', $stage)); +// } +// +// return true; +// } +// +// /** +// * @param ImportJob $job +// */ +// public function setJob(ImportJob $job) +// { +// $this->job = $job; +// $this->repository = app(ImportJobRepositoryInterface::class); +// $this->repository->setUser($job->user); +// } +// +// /** +// * @return Customer +// * +// * @throws \FireflyIII\Exceptions\FireflyException +// * @throws \FireflyIII\Services\Spectre\Exception\SpectreException +// * @throws \Illuminate\Container\EntryNotFoundException +// */ +// protected function createCustomer(): Customer +// { +// $newCustomerRequest = new NewCustomerRequest($this->job->user); +// $customer = null; +// try { +// $newCustomerRequest->call(); +// $customer = $newCustomerRequest->getCustomer(); +// } catch (Exception $e) { +// // already exists, must fetch customer instead. +// Log::warning(sprintf('Customer exists already for user, fetch it: %s', $e->getMessage())); +// } +// if (null === $customer) { +// $getCustomerRequest = new ListCustomersRequest($this->job->user); +// $getCustomerRequest->call(); +// $customers = $getCustomerRequest->getCustomers(); +// /** @var Customer $current */ +// foreach ($customers as $current) { +// if ('default_ff3_customer' === $current->getIdentifier()) { +// $customer = $current; +// break; +// } +// } +// } +// +// Preferences::setForUser($this->job->user, 'spectre_customer', $customer->toArray()); +// +// return $customer; +// } +// +// /** +// * @return Customer +// * +// * @throws FireflyException +// * @throws SpectreException +// * @throws \Illuminate\Container\EntryNotFoundException +// */ +// protected function getCustomer(): Customer +// { +// $config = $this->getConfig(); +// if (null !== $config['customer']) { +// $customer = new Customer($config['customer']); +// +// return $customer; +// } +// +// $customer = $this->createCustomer(); +// $config['customer'] = [ +// 'id' => $customer->getId(), +// 'identifier' => $customer->getIdentifier(), +// 'secret' => $customer->getSecret(), +// ]; +// $this->setConfig($config); +// +// return $customer; +// } +// +// /** +// * @param Customer $customer +// * @param string $returnUri +// * +// * @return Token +// * +// * @throws \FireflyIII\Exceptions\FireflyException +// * @throws \FireflyIII\Services\Spectre\Exception\SpectreException +// * @throws \Illuminate\Container\EntryNotFoundException +// */ +// protected function getToken(Customer $customer, string $returnUri): Token +// { +// $request = new CreateTokenRequest($this->job->user); +// $request->setUri($returnUri); +// $request->setCustomer($customer); +// $request->call(); +// Log::debug('Call to get token is finished'); +// +// return $request->getToken(); +// } +// +// /** +// * @throws FireflyException +// * @throws SpectreException +// * @throws \Illuminate\Container\EntryNotFoundException +// */ +// protected function runStageInitial(): void +// { +// Log::debug('In runStageInitial()'); +// +// // create customer if user does not have one: +// $customer = $this->getCustomer(); +// Log::debug(sprintf('Customer ID is %s', $customer->getId())); +// +// // use customer to request a token: +// $uri = route('import.status', [$this->job->key]); +// $token = $this->getToken($customer, $uri); +// Log::debug(sprintf('Token is %s', $token->getToken())); +// +// // update job, give it the token: +// $config = $this->getConfig(); +// $config['has-token'] = true; +// $config['token'] = $token->getToken(); +// $config['token-expires'] = $token->getExpiresAt()->format('U'); +// $config['token-url'] = $token->getConnectUrl(); +// $config['stage'] = 'has-token'; +// $this->setConfig($config); +// +// Log::debug('Job config is now', $config); +// +// // update job, set status to "configuring". +// $this->setStatus('configuring'); +// Log::debug(sprintf('Job status is now %s', $this->job->status)); +// $this->addStep(); +// } +// +// /** +// * @throws FireflyException +// * @throws SpectreException +// * @throws \Illuminate\Container\EntryNotFoundException +// */ +// protected function runStageLoggedIn(): void +// { +// Log::debug('In runStageLoggedIn'); +// // list all logins: +// $customer = $this->getCustomer(); +// $request = new ListLoginsRequest($this->job->user); +// $request->setCustomer($customer); +// $request->call(); +// +// $logins = $request->getLogins(); +// /** @var Login $final */ +// $final = null; +// // loop logins, find the latest with no error in it: +// $time = 0; +// /** @var Login $login */ +// foreach ($logins as $login) { +// $attempt = $login->getLastAttempt(); +// $attemptTime = (int)$attempt->getCreatedAt()->format('U'); +// if ($attemptTime > $time && null === $attempt->getFailErrorClass()) { +// $time = $attemptTime; +// $final = $login; +// } +// } +// if (null === $final) { +// Log::error('Could not find a valid login for this user.'); +// $this->repository->addError($this->job, 0, 'Spectre connection failed. Did you use invalid credentials, press Cancel or failed the 2FA challenge?'); +// $this->repository->setStatus($this->job, 'error'); +// +// return; +// } +// $this->addStep(); +// +// // list the users accounts using this login. +// $accountRequest = new ListAccountsRequest($this->job->user); +// $accountRequest->setLogin($login); +// $accountRequest->call(); +// $accounts = $accountRequest->getAccounts(); +// +// // store accounts in job: +// $all = []; +// /** @var Account $account */ +// foreach ($accounts as $account) { +// $all[] = $account->toArray(); +// } +// +// // update job: +// $config = $this->getConfig(); +// $config['accounts'] = $all; +// $config['login'] = $login->toArray(); +// $config['stage'] = 'have-accounts'; +// +// $this->setConfig($config); +// $this->setStatus('configuring'); +// $this->addStep(); +// } +// +// /** +// * Shorthand method. +// */ +// private function addStep() +// { +// $this->repository->addStepsDone($this->job, 1); +// } +// +// /** +// * Shorthand +// * +// * @param int $steps +// */ +// private function addTotalSteps(int $steps) +// { +// $this->repository->addTotalSteps($this->job, $steps); +// } +// +// /** +// * @return array +// */ +// private function getConfig(): array +// { +// return $this->repository->getConfiguration($this->job); +// } +// +// /** +// * Shorthand method. +// * +// * @return array +// */ +// private function getExtendedStatus(): array +// { +// return $this->repository->getExtendedStatus($this->job); +// } +// +// /** +// * Shorthand method. +// * +// * @return string +// */ +// private function getStatus(): string +// { +// return $this->repository->getStatus($this->job); +// } +// +// /** +// * @param array $all +// * +// * @throws FireflyException +// */ +// private function importTransactions(array $all) +// { +// Log::debug('Going to import transactions'); +// $collection = new Collection; +// // create import objects? +// foreach ($all as $accountId => $data) { +// Log::debug(sprintf('Now at account #%d', $accountId)); +// /** @var Transaction $transaction */ +// foreach ($data['transactions'] as $transaction) { +// Log::debug(sprintf('Now at transaction #%d', $transaction->getId())); +// /** @var Account $account */ +// $account = $data['account']; +// $importJournal = new ImportJournal; +// $importJournal->setUser($this->job->user); +// $importJournal->asset->setDefaultAccountId($data['import_id']); +// // call set value a bunch of times for various data entries: +// $tags = []; +// $tags[] = $transaction->getMode(); +// $tags[] = $transaction->getStatus(); +// if ($transaction->isDuplicated()) { +// $tags[] = 'possibly-duplicated'; +// } +// $extra = $transaction->getExtra()->toArray(); +// $notes = ''; +// // double space for newline in Markdown. +// $notes .= (string)trans('import.imported_from_account', ['account' => $account->getName()]) . ' ' . "\n"; +// +// foreach ($extra as $key => $value) { +// switch ($key) { +// case 'account_number': +// $importJournal->setValue(['role' => 'account-number', 'value' => $value]); +// break; +// case 'original_category': +// case 'original_subcategory': +// case 'customer_category_code': +// case 'customer_category_name': +// $tags[] = $value; +// break; +// case 'payee': +// $importJournal->setValue(['role' => 'opposing-name', 'value' => $value]); +// break; +// case 'original_amount': +// $importJournal->setValue(['role' => 'amount_foreign', 'value' => $value]); +// break; +// case 'original_currency_code': +// $importJournal->setValue(['role' => 'foreign-currency-code', 'value' => $value]); +// break; +// default: +// $notes .= $key . ': ' . $value . ' '; // for newline in Markdown. +// } +// } +// // hash +// $importJournal->setHash($transaction->getHash()); +// +// // account ID (Firefly III account): +// $importJournal->setValue(['role' => 'account-id', 'value' => $data['import_id'], 'mapped' => $data['import_id']]); +// +// // description: +// $importJournal->setValue(['role' => 'description', 'value' => $transaction->getDescription()]); +// +// // date: +// $importJournal->setValue(['role' => 'date-transaction', 'value' => $transaction->getMadeOn()->toIso8601String()]); +// +// // amount +// $importJournal->setValue(['role' => 'amount', 'value' => $transaction->getAmount()]); +// $importJournal->setValue(['role' => 'currency-code', 'value' => $transaction->getCurrencyCode()]); +// +// // various meta fields: +// $importJournal->setValue(['role' => 'category-name', 'value' => $transaction->getCategory()]); +// $importJournal->setValue(['role' => 'note', 'value' => $notes]); +// $importJournal->setValue(['role' => 'tags-comma', 'value' => implode(',', $tags)]); +// $collection->push($importJournal); +// } +// } +// $this->addStep(); +// Log::debug(sprintf('Going to try and store all %d them.', $collection->count())); +// +// $this->addTotalSteps(7 * $collection->count()); +// // try to store them (seven steps per transaction) +// $storage = new ImportStorage; +// +// $storage->setJob($this->job); +// $storage->setDateFormat('Y-m-d\TH:i:sO'); +// $storage->setObjects($collection); +// $storage->store(); +// Log::info('Back in importTransactions()'); +// +// // link to tag +// /** @var TagRepositoryInterface $repository */ +// $repository = app(TagRepositoryInterface::class); +// $repository->setUser($this->job->user); +// $data = [ +// 'tag' => trans('import.import_with_key', ['key' => $this->job->key]), +// 'date' => new Carbon, +// 'description' => null, +// 'latitude' => null, +// 'longitude' => null, +// 'zoomLevel' => null, +// 'tagMode' => 'nothing', +// ]; +// $tag = $repository->store($data); +// $extended = $this->getExtendedStatus(); +// $extended['tag'] = $tag->id; +// $this->setExtendedStatus($extended); +// +// Log::debug(sprintf('Created tag #%d ("%s")', $tag->id, $tag->tag)); +// Log::debug('Looping journals...'); +// $journalIds = $storage->journals->pluck('id')->toArray(); +// $tagId = $tag->id; +// $this->addTotalSteps(\count($journalIds)); +// +// foreach ($journalIds as $journalId) { +// Log::debug(sprintf('Linking journal #%d to tag #%d...', $journalId, $tagId)); +// DB::table('tag_transaction_journal')->insert(['transaction_journal_id' => $journalId, 'tag_id' => $tagId]); +// $this->addStep(); +// } +// Log::info(sprintf('Linked %d journals to tag #%d ("%s")', $storage->journals->count(), $tag->id, $tag->tag)); +// +// // set status to "finished"? +// // update job: +// $this->setStatus('finished'); +// $this->addStep(); +// +// } +// +// /** +// * @throws FireflyException +// * @throws SpectreException +// * @throws \Illuminate\Container\EntryNotFoundException +// */ +// private function runStageHaveMapping() +// { +// $config = $this->getConfig(); +// $accounts = $config['accounts'] ?? []; +// $all = []; +// $count = 0; +// /** @var array $accountArray */ +// foreach ($accounts as $accountArray) { +// $account = new Account($accountArray); +// $importId = (int)($config['accounts-mapped'][$account->getId()] ?? 0.0); +// $doImport = 0 !== $importId; +// if (!$doImport) { +// Log::debug(sprintf('Will NOT import from Spectre account #%d ("%s")', $account->getId(), $account->getName())); +// continue; +// } +// // grab all transactions +// $listTransactionsRequest = new ListTransactionsRequest($this->job->user); +// $listTransactionsRequest->setAccount($account); +// $listTransactionsRequest->call(); +// $transactions = $listTransactionsRequest->getTransactions(); +// $all[$account->getId()] = [ +// 'account' => $account, +// 'import_id' => $importId, +// 'transactions' => $transactions, +// ]; +// $count += \count($transactions); +// } +// Log::debug(sprintf('Total number of transactions: %d', $count)); +// $this->addStep(); +// +// $this->importTransactions($all); +// } +// +// /** +// * Shorthand. +// * +// * @param array $config +// */ +// private function setConfig(array $config): void +// { +// $this->repository->setConfiguration($this->job, $config); +// +// } +// +// /** +// * Shorthand method. +// * +// * @param array $extended +// */ +// private function setExtendedStatus(array $extended): void +// { +// $this->repository->setExtendedStatus($this->job, $extended); +// +// } +// +// /** +// * Shorthand. +// * +// * @param string $status +// */ +// private function setStatus(string $status): void +// { +// $this->repository->setStatus($this->job, $status); +// } /** - * ImportRoutine constructor. - */ - public function __construct() - { - $this->journals = new Collection; - $this->errors = new Collection; - } - - /** - * @return Collection - */ - public function getErrors(): Collection - { - return $this->errors; - } - - /** - * @return Collection - */ - public function getJournals(): Collection - { - return $this->journals; - } - - /** - * @return int - */ - public function getLines(): int - { - return $this->lines; - } - - /** - * A Spectre job that ends up here is either "configured" or "running", and will be set to "running" - * when it is "configured". - * - * Job has several stages, stored in extended status key 'stage' - * - * initial: just begun, nothing happened. action: get a customer and a token. Next status: has-token - * has-token: redirect user to sandstorm, make user login. set job to: user-logged-in - * user-logged-in: customer has an attempt. action: analyse/get attempt and go for next status. - * if attempt failed: job status is error, save a warning somewhere? - * if success, try to get accounts. Save in config key 'accounts'. set status: have-accounts and "configuring" - * - * have-accounts: make user link accounts and select accounts to import from. - * - * If job is "configuring" and stage "have-accounts" then present the accounts and make user link them to - * own asset accounts. Store this mapping, set config to "have-account-mapping" and job status configured". - * - * have-account-mapping: start downloading transactions? + * At the end of each run(), the import routine must set the job to the expected status. * + * The final status of the routine must be "provider_finished". * * @return bool - * * @throws FireflyException - * @throws SpectreException - * @throws \Illuminate\Container\EntryNotFoundException */ - public function run(): bool + public function run(): void { - if ('configured' === $this->getStatus()) { - $this->repository->updateStatus($this->job, 'running'); - } - Log::info(sprintf('Start with import job %s using Spectre.', $this->job->key)); - set_time_limit(0); - - // check if job has token first! - $stage = $this->getConfig()['stage'] ?? 'unknown'; - - switch ($stage) { - case 'initial': - // get customer and token: - $this->runStageInitial(); - break; - case 'has-token': - // import routine does nothing at this point: - break; - case 'user-logged-in': - $this->runStageLoggedIn(); - break; - case 'have-account-mapping': - $this->runStageHaveMapping(); - break; - default: - throw new FireflyException(sprintf('Cannot handle stage %s', $stage)); - } - - return true; + // TODO: Implement run() method. + throw new NotImplementedException; } /** * @param ImportJob $job + * + * @return mixed */ public function setJob(ImportJob $job) { - $this->job = $job; - $this->repository = app(ImportJobRepositoryInterface::class); - $this->repository->setUser($job->user); - } - - /** - * @return Customer - * - * @throws \FireflyIII\Exceptions\FireflyException - * @throws \FireflyIII\Services\Spectre\Exception\SpectreException - * @throws \Illuminate\Container\EntryNotFoundException - */ - protected function createCustomer(): Customer - { - $newCustomerRequest = new NewCustomerRequest($this->job->user); - $customer = null; - try { - $newCustomerRequest->call(); - $customer = $newCustomerRequest->getCustomer(); - } catch (Exception $e) { - // already exists, must fetch customer instead. - Log::warning(sprintf('Customer exists already for user, fetch it: %s', $e->getMessage())); - } - if (null === $customer) { - $getCustomerRequest = new ListCustomersRequest($this->job->user); - $getCustomerRequest->call(); - $customers = $getCustomerRequest->getCustomers(); - /** @var Customer $current */ - foreach ($customers as $current) { - if ('default_ff3_customer' === $current->getIdentifier()) { - $customer = $current; - break; - } - } - } - - Preferences::setForUser($this->job->user, 'spectre_customer', $customer->toArray()); - - return $customer; - } - - /** - * @return Customer - * - * @throws FireflyException - * @throws SpectreException - * @throws \Illuminate\Container\EntryNotFoundException - */ - protected function getCustomer(): Customer - { - $config = $this->getConfig(); - if (null !== $config['customer']) { - $customer = new Customer($config['customer']); - - return $customer; - } - - $customer = $this->createCustomer(); - $config['customer'] = [ - 'id' => $customer->getId(), - 'identifier' => $customer->getIdentifier(), - 'secret' => $customer->getSecret(), - ]; - $this->setConfig($config); - - return $customer; - } - - /** - * @param Customer $customer - * @param string $returnUri - * - * @return Token - * - * @throws \FireflyIII\Exceptions\FireflyException - * @throws \FireflyIII\Services\Spectre\Exception\SpectreException - * @throws \Illuminate\Container\EntryNotFoundException - */ - protected function getToken(Customer $customer, string $returnUri): Token - { - $request = new CreateTokenRequest($this->job->user); - $request->setUri($returnUri); - $request->setCustomer($customer); - $request->call(); - Log::debug('Call to get token is finished'); - - return $request->getToken(); - } - - /** - * @throws FireflyException - * @throws SpectreException - * @throws \Illuminate\Container\EntryNotFoundException - */ - protected function runStageInitial(): void - { - Log::debug('In runStageInitial()'); - - // create customer if user does not have one: - $customer = $this->getCustomer(); - Log::debug(sprintf('Customer ID is %s', $customer->getId())); - - // use customer to request a token: - $uri = route('import.status', [$this->job->key]); - $token = $this->getToken($customer, $uri); - Log::debug(sprintf('Token is %s', $token->getToken())); - - // update job, give it the token: - $config = $this->getConfig(); - $config['has-token'] = true; - $config['token'] = $token->getToken(); - $config['token-expires'] = $token->getExpiresAt()->format('U'); - $config['token-url'] = $token->getConnectUrl(); - $config['stage'] = 'has-token'; - $this->setConfig($config); - - Log::debug('Job config is now', $config); - - // update job, set status to "configuring". - $this->setStatus('configuring'); - Log::debug(sprintf('Job status is now %s', $this->job->status)); - $this->addStep(); - } - - /** - * @throws FireflyException - * @throws SpectreException - * @throws \Illuminate\Container\EntryNotFoundException - */ - protected function runStageLoggedIn(): void - { - Log::debug('In runStageLoggedIn'); - // list all logins: - $customer = $this->getCustomer(); - $request = new ListLoginsRequest($this->job->user); - $request->setCustomer($customer); - $request->call(); - - $logins = $request->getLogins(); - /** @var Login $final */ - $final = null; - // loop logins, find the latest with no error in it: - $time = 0; - /** @var Login $login */ - foreach ($logins as $login) { - $attempt = $login->getLastAttempt(); - $attemptTime = (int)$attempt->getCreatedAt()->format('U'); - if ($attemptTime > $time && null === $attempt->getFailErrorClass()) { - $time = $attemptTime; - $final = $login; - } - } - if (null === $final) { - Log::error('Could not find a valid login for this user.'); - $this->repository->addError($this->job, 0, 'Spectre connection failed. Did you use invalid credentials, press Cancel or failed the 2FA challenge?'); - $this->repository->setStatus($this->job, 'error'); - - return; - } - $this->addStep(); - - // list the users accounts using this login. - $accountRequest = new ListAccountsRequest($this->job->user); - $accountRequest->setLogin($login); - $accountRequest->call(); - $accounts = $accountRequest->getAccounts(); - - // store accounts in job: - $all = []; - /** @var Account $account */ - foreach ($accounts as $account) { - $all[] = $account->toArray(); - } - - // update job: - $config = $this->getConfig(); - $config['accounts'] = $all; - $config['login'] = $login->toArray(); - $config['stage'] = 'have-accounts'; - - $this->setConfig($config); - $this->setStatus('configuring'); - $this->addStep(); - } - - /** - * Shorthand method. - */ - private function addStep() - { - $this->repository->addStepsDone($this->job, 1); - } - - /** - * Shorthand - * - * @param int $steps - */ - private function addTotalSteps(int $steps) - { - $this->repository->addTotalSteps($this->job, $steps); - } - - /** - * @return array - */ - private function getConfig(): array - { - return $this->repository->getConfiguration($this->job); - } - - /** - * Shorthand method. - * - * @return array - */ - private function getExtendedStatus(): array - { - return $this->repository->getExtendedStatus($this->job); - } - - /** - * Shorthand method. - * - * @return string - */ - private function getStatus(): string - { - return $this->repository->getStatus($this->job); - } - - /** - * @param array $all - * - * @throws FireflyException - */ - private function importTransactions(array $all) - { - Log::debug('Going to import transactions'); - $collection = new Collection; - // create import objects? - foreach ($all as $accountId => $data) { - Log::debug(sprintf('Now at account #%d', $accountId)); - /** @var Transaction $transaction */ - foreach ($data['transactions'] as $transaction) { - Log::debug(sprintf('Now at transaction #%d', $transaction->getId())); - /** @var Account $account */ - $account = $data['account']; - $importJournal = new ImportJournal; - $importJournal->setUser($this->job->user); - $importJournal->asset->setDefaultAccountId($data['import_id']); - // call set value a bunch of times for various data entries: - $tags = []; - $tags[] = $transaction->getMode(); - $tags[] = $transaction->getStatus(); - if ($transaction->isDuplicated()) { - $tags[] = 'possibly-duplicated'; - } - $extra = $transaction->getExtra()->toArray(); - $notes = ''; - // double space for newline in Markdown. - $notes .= (string)trans('import.imported_from_account', ['account' => $account->getName()]) . ' ' . "\n"; - - foreach ($extra as $key => $value) { - switch ($key) { - case 'account_number': - $importJournal->setValue(['role' => 'account-number', 'value' => $value]); - break; - case 'original_category': - case 'original_subcategory': - case 'customer_category_code': - case 'customer_category_name': - $tags[] = $value; - break; - case 'payee': - $importJournal->setValue(['role' => 'opposing-name', 'value' => $value]); - break; - case 'original_amount': - $importJournal->setValue(['role' => 'amount_foreign', 'value' => $value]); - break; - case 'original_currency_code': - $importJournal->setValue(['role' => 'foreign-currency-code', 'value' => $value]); - break; - default: - $notes .= $key . ': ' . $value . ' '; // for newline in Markdown. - } - } - // hash - $importJournal->setHash($transaction->getHash()); - - // account ID (Firefly III account): - $importJournal->setValue(['role' => 'account-id', 'value' => $data['import_id'], 'mapped' => $data['import_id']]); - - // description: - $importJournal->setValue(['role' => 'description', 'value' => $transaction->getDescription()]); - - // date: - $importJournal->setValue(['role' => 'date-transaction', 'value' => $transaction->getMadeOn()->toIso8601String()]); - - // amount - $importJournal->setValue(['role' => 'amount', 'value' => $transaction->getAmount()]); - $importJournal->setValue(['role' => 'currency-code', 'value' => $transaction->getCurrencyCode()]); - - // various meta fields: - $importJournal->setValue(['role' => 'category-name', 'value' => $transaction->getCategory()]); - $importJournal->setValue(['role' => 'note', 'value' => $notes]); - $importJournal->setValue(['role' => 'tags-comma', 'value' => implode(',', $tags)]); - $collection->push($importJournal); - } - } - $this->addStep(); - Log::debug(sprintf('Going to try and store all %d them.', $collection->count())); - - $this->addTotalSteps(7 * $collection->count()); - // try to store them (seven steps per transaction) - $storage = new ImportStorage; - - $storage->setJob($this->job); - $storage->setDateFormat('Y-m-d\TH:i:sO'); - $storage->setObjects($collection); - $storage->store(); - Log::info('Back in importTransactions()'); - - // link to tag - /** @var TagRepositoryInterface $repository */ - $repository = app(TagRepositoryInterface::class); - $repository->setUser($this->job->user); - $data = [ - 'tag' => trans('import.import_with_key', ['key' => $this->job->key]), - 'date' => new Carbon, - 'description' => null, - 'latitude' => null, - 'longitude' => null, - 'zoomLevel' => null, - 'tagMode' => 'nothing', - ]; - $tag = $repository->store($data); - $extended = $this->getExtendedStatus(); - $extended['tag'] = $tag->id; - $this->setExtendedStatus($extended); - - Log::debug(sprintf('Created tag #%d ("%s")', $tag->id, $tag->tag)); - Log::debug('Looping journals...'); - $journalIds = $storage->journals->pluck('id')->toArray(); - $tagId = $tag->id; - $this->addTotalSteps(\count($journalIds)); - - foreach ($journalIds as $journalId) { - Log::debug(sprintf('Linking journal #%d to tag #%d...', $journalId, $tagId)); - DB::table('tag_transaction_journal')->insert(['transaction_journal_id' => $journalId, 'tag_id' => $tagId]); - $this->addStep(); - } - Log::info(sprintf('Linked %d journals to tag #%d ("%s")', $storage->journals->count(), $tag->id, $tag->tag)); - - // set status to "finished"? - // update job: - $this->setStatus('finished'); - $this->addStep(); - - } - - /** - * @throws FireflyException - * @throws SpectreException - * @throws \Illuminate\Container\EntryNotFoundException - */ - private function runStageHaveMapping() - { - $config = $this->getConfig(); - $accounts = $config['accounts'] ?? []; - $all = []; - $count = 0; - /** @var array $accountArray */ - foreach ($accounts as $accountArray) { - $account = new Account($accountArray); - $importId = (int)($config['accounts-mapped'][$account->getId()] ?? 0.0); - $doImport = 0 !== $importId; - if (!$doImport) { - Log::debug(sprintf('Will NOT import from Spectre account #%d ("%s")', $account->getId(), $account->getName())); - continue; - } - // grab all transactions - $listTransactionsRequest = new ListTransactionsRequest($this->job->user); - $listTransactionsRequest->setAccount($account); - $listTransactionsRequest->call(); - $transactions = $listTransactionsRequest->getTransactions(); - $all[$account->getId()] = [ - 'account' => $account, - 'import_id' => $importId, - 'transactions' => $transactions, - ]; - $count += \count($transactions); - } - Log::debug(sprintf('Total number of transactions: %d', $count)); - $this->addStep(); - - $this->importTransactions($all); - } - - /** - * Shorthand. - * - * @param array $config - */ - private function setConfig(array $config): void - { - $this->repository->setConfiguration($this->job, $config); - - } - - /** - * Shorthand method. - * - * @param array $extended - */ - private function setExtendedStatus(array $extended): void - { - $this->repository->setExtendedStatus($this->job, $extended); - - } - - /** - * Shorthand. - * - * @param string $status - */ - private function setStatus(string $status): void - { - $this->repository->setStatus($this->job, $status); + // TODO: Implement setJob() method. + throw new NotImplementedException; } } diff --git a/app/Import/Storage/ImportArrayStorage.php b/app/Import/Storage/ImportArrayStorage.php index 2d08c9631a..99ee41171d 100644 --- a/app/Import/Storage/ImportArrayStorage.php +++ b/app/Import/Storage/ImportArrayStorage.php @@ -3,21 +3,21 @@ namespace FireflyIII\Import\Storage; use Carbon\Carbon; +use DB; use FireflyIII\Exceptions\FireflyException; use FireflyIII\Factory\TransactionJournalFactory; use FireflyIII\Helpers\Collector\JournalCollectorInterface; use FireflyIII\Helpers\Filter\InternalTransferFilter; use FireflyIII\Models\ImportJob; -use FireflyIII\Models\Tag; +use FireflyIII\Models\Rule; use FireflyIII\Models\Transaction; use FireflyIII\Models\TransactionJournalMeta; use FireflyIII\Models\TransactionType; use FireflyIII\Repositories\ImportJob\ImportJobRepositoryInterface; use FireflyIII\Repositories\Tag\TagRepositoryInterface; +use FireflyIII\TransactionRules\Processor; use Illuminate\Support\Collection; -use Illuminate\Support\MessageBag; use Log; -use DB; /** * Creates new transactions based upon arrays. Will first check the array for duplicates. @@ -40,11 +40,9 @@ class ImportArrayStorage private $transfers; /** - * ImportArrayStorage constructor. - * * @param ImportJob $importJob */ - public function __construct(ImportJob $importJob) + public function setJob(ImportJob $importJob): void { $this->importJob = $importJob; $this->countTransfers(); @@ -56,104 +54,61 @@ class ImportArrayStorage } /** - * Actually does the storing. + * Actually does the storing. Does three things. + * - Store journals + * - Link to tag + * - Run rules (if set to) * * @return Collection * @throws FireflyException */ public function store(): Collection { - $count = count($this->importJob->transactions); - Log::debug(sprintf('Now in store(). Count of items is %d', $count)); - $toStore = []; - foreach ($this->importJob->transactions as $index => $transaction) { - Log::debug(sprintf('Now at item %d out of %d', ($index + 1), $count)); - $existingId = $this->hashExists($transaction); - if (null !== $existingId) { - $this->logDuplicateObject($transaction, $existingId); - $this->repository->addErrorMessage( - $this->importJob, sprintf( - 'Entry #%d ("%s") could not be imported. It already exists.', - $index, $transaction['description'] - ) - ); - continue; - } - if ($this->checkForTransfers) { - if ($this->transferExists($transaction)) { - $this->logDuplicateTransfer($transaction); - $this->repository->addErrorMessage( - $this->importJob, sprintf( - 'Entry #%d ("%s") could not be imported. Such a transfer already exists.', - $index, - $transaction['description'] - ) - ); - continue; - } - } - $toStore[] = $transaction; + // store transactions + $this->setStatus('storing_data'); + $collection = $this->storeArray(); + $this->setStatus('stored_data'); + + // link tag: + $this->setStatus('linking_to_tag'); + $this->linkToTag($collection); + $this->setStatus('linked_to_tag'); + + // run rules, if configured to. + $config = $this->importJob->configuration; + if (isset($config['apply-rules']) && $config['apply-rules'] === true) { + $this->setStatus('applying_rules'); + $this->applyRules($collection); + $this->setStatus('rules_applied'); } - if (count($toStore) === 0) { - Log::info('No transactions to store left!'); - - return new Collection; - } - Log::debug('Going to store...'); - // now actually store them: - $collection = new Collection; - /** @var TransactionJournalFactory $factory */ - $factory = app(TransactionJournalFactory::class); - $factory->setUser($this->importJob->user); - foreach ($toStore as $store) { - // convert the date to an object: - $store['date'] = Carbon::createFromFormat('Y-m-d', $store['date']); - - // store the journal. - $collection->push($factory->create($store)); - } - Log::debug('DONE storing!'); - - - // create tag and append journals: - $this->createTag($collection); - return $collection; } /** * @param Collection $collection + * + * @throws FireflyException */ - private function createTag(Collection $collection): void + private function applyRules(Collection $collection): void { + $rules = $this->getRules(); + if ($rules->count() > 0) { + foreach ($collection as $journal) { + $rules->each( + function (Rule $rule) use ($journal) { + Log::debug(sprintf('Going to apply rule #%d to journal %d.', $rule->id, $journal->id)); + $processor = Processor::make($rule); + $processor->handleTransactionJournal($journal); + if ($rule->stop_processing) { + return false; + } - /** @var TagRepositoryInterface $repository */ - $repository = app(TagRepositoryInterface::class); - $repository->setUser($this->importJob->user); - $data = [ - 'tag' => trans('import.import_with_key', ['key' => $this->importJob->key]), - 'date' => new Carbon, - 'description' => null, - 'latitude' => null, - 'longitude' => null, - 'zoomLevel' => null, - 'tagMode' => 'nothing', - ]; - $tag = $repository->store($data); - - Log::debug(sprintf('Created tag #%d ("%s")', $tag->id, $tag->tag)); - Log::debug('Looping journals...'); - $journalIds = $collection->pluck('id')->toArray(); - $tagId = $tag->id; - foreach ($journalIds as $journalId) { - Log::debug(sprintf('Linking journal #%d to tag #%d...', $journalId, $tagId)); - DB::table('tag_transaction_journal')->insert(['transaction_journal_id' => $journalId, 'tag_id' => $tagId]); + return true; + } + ); + } } - Log::info(sprintf('Linked %d journals to tag #%d ("%s")', $collection->count(), $tag->id, $tag->tag)); - - $this->repository->setTag($this->importJob, $tag); - } /** @@ -161,8 +116,10 @@ class ImportArrayStorage */ private function countTransfers(): void { + /** @var array $array */ + $array = $this->importJob->transactions; $count = 0; - foreach ($this->importJob->transactions as $transaction) { + foreach ($array as $transaction) { if (strtolower(TransactionType::TRANSFER) === $transaction['type']) { $count++; } @@ -177,6 +134,29 @@ class ImportArrayStorage } + /** + * @return Collection + */ + private function getRules(): Collection + { + /** @var Collection $set */ + $set = Rule::distinct() + ->where('rules.user_id', $this->importJob->user_id) + ->leftJoin('rule_groups', 'rule_groups.id', '=', 'rules.rule_group_id') + ->leftJoin('rule_triggers', 'rules.id', '=', 'rule_triggers.rule_id') + ->where('rule_groups.active', 1) + ->where('rule_triggers.trigger_type', 'user_action') + ->where('rule_triggers.trigger_value', 'store-journal') + ->where('rules.active', 1) + ->orderBy('rule_groups.order', 'ASC') + ->orderBy('rules.order', 'ASC') + ->get(['rules.*', 'rule_groups.order']); + + Log::debug(sprintf('Found %d user rules.', $set->count())); + + return $set; + } + /** * Get the users transfers, so they can be compared to whatever the user is trying to import. */ @@ -192,7 +172,6 @@ class ImportArrayStorage } - /** * @param array $transaction * @@ -222,6 +201,39 @@ class ImportArrayStorage return (int)$entry->transaction_journal_id; } + /** + * @param Collection $collection + */ + private function linkToTag(Collection $collection): void + { + /** @var TagRepositoryInterface $repository */ + $repository = app(TagRepositoryInterface::class); + $repository->setUser($this->importJob->user); + $data = [ + 'tag' => trans('import.import_with_key', ['key' => $this->importJob->key]), + 'date' => new Carbon, + 'description' => null, + 'latitude' => null, + 'longitude' => null, + 'zoomLevel' => null, + 'tagMode' => 'nothing', + ]; + $tag = $repository->store($data); + + Log::debug(sprintf('Created tag #%d ("%s")', $tag->id, $tag->tag)); + Log::debug('Looping journals...'); + $journalIds = $collection->pluck('id')->toArray(); + $tagId = $tag->id; + foreach ($journalIds as $journalId) { + Log::debug(sprintf('Linking journal #%d to tag #%d...', $journalId, $tagId)); + DB::table('tag_transaction_journal')->insert(['transaction_journal_id' => $journalId, 'tag_id' => $tagId]); + } + Log::info(sprintf('Linked %d journals to tag #%d ("%s")', $collection->count(), $tag->id, $tag->tag)); + + $this->repository->setTag($this->importJob, $tag); + + } + /** * @param array $transaction * @param int $existingId @@ -255,6 +267,84 @@ class ImportArrayStorage ); } + /** + * Shorthand method to quickly set job status + * + * @param string $status + */ + private function setStatus(string $status): void + { + $this->repository->setStatus($this->importJob, $status); + } + + /** + * Store array as journals. + * + * @return Collection + * @throws FireflyException + */ + private function storeArray(): Collection + { + /** @var array $array */ + $array = $this->importJob->transactions; + $count = \count($array); + $toStore = []; + + Log::debug(sprintf('Now in store(). Count of items is %d', $count)); + + foreach ($array as $index => $transaction) { + Log::debug(sprintf('Now at item %d out of %d', $index + 1, $count)); + $existingId = $this->hashExists($transaction); + if (null !== $existingId) { + $this->logDuplicateObject($transaction, $existingId); + $this->repository->addErrorMessage( + $this->importJob, sprintf( + 'Entry #%d ("%s") could not be imported. It already exists.', + $index, $transaction['description'] + ) + ); + continue; + } + if ($this->checkForTransfers) { + if ($this->transferExists($transaction)) { + $this->logDuplicateTransfer($transaction); + $this->repository->addErrorMessage( + $this->importJob, sprintf( + 'Entry #%d ("%s") could not be imported. Such a transfer already exists.', + $index, + $transaction['description'] + ) + ); + continue; + } + } + $toStore[] = $transaction; + } + + if (\count($toStore) === 0) { + Log::info('No transactions to store left!'); + + return new Collection; + } + + Log::debug('Going to store...'); + // now actually store them: + $collection = new Collection; + /** @var TransactionJournalFactory $factory */ + $factory = app(TransactionJournalFactory::class); + $factory->setUser($this->importJob->user); + foreach ($toStore as $store) { + // convert the date to an object: + $store['date'] = Carbon::createFromFormat('Y-m-d', $store['date']); + + // store the journal. + $collection->push($factory->create($store)); + } + Log::debug('DONE storing!'); + + return $collection; + } + /** * Check if a transfer exists. * diff --git a/app/Support/Import/Routine/Fake/StageAhoyHandler.php b/app/Support/Import/Routine/Fake/StageAhoyHandler.php index 851b61ce06..32bbad15fd 100644 --- a/app/Support/Import/Routine/Fake/StageAhoyHandler.php +++ b/app/Support/Import/Routine/Fake/StageAhoyHandler.php @@ -36,7 +36,7 @@ class StageAhoyHandler */ public function run(): void { - for ($i = 0; $i < 15; $i++) { + for ($i = 0; $i < 5; $i++) { Log::debug(sprintf('Am now in stage AHOY hander, sleeping... (%d)', $i)); sleep(1); } diff --git a/app/Support/Import/Routine/Fake/StageNewHandler.php b/app/Support/Import/Routine/Fake/StageNewHandler.php index d12ef19f4f..3d31a008f5 100644 --- a/app/Support/Import/Routine/Fake/StageNewHandler.php +++ b/app/Support/Import/Routine/Fake/StageNewHandler.php @@ -36,7 +36,7 @@ class StageNewHandler */ public function run(): void { - for ($i = 0; $i < 15; $i++) { + for ($i = 0; $i < 5; $i++) { Log::debug(sprintf('Am now in stage new hander, sleeping... (%d)', $i)); sleep(1); } diff --git a/config/import.php b/config/import.php index 7ad5464dd5..3cab291af3 100644 --- a/config/import.php +++ b/config/import.php @@ -39,7 +39,7 @@ use FireflyIII\Import\Routine\SpectreRoutine; return [ 'enabled' => [ - 'fake' => true, + 'fake' => false, 'file' => true, 'bunq' => true, 'spectre' => true, diff --git a/database/migrations/2018_04_29_174524_changes_for_v474.php b/database/migrations/2018_04_29_174524_changes_for_v474.php index 4b527e5ea7..d47561bcc9 100644 --- a/database/migrations/2018_04_29_174524_changes_for_v474.php +++ b/database/migrations/2018_04_29_174524_changes_for_v474.php @@ -30,8 +30,8 @@ class ChangesForV474 extends Migration function (Blueprint $table) { $table->string('provider', 50)->after('file_type')->default(''); $table->string('stage', 50)->after('status')->default(''); - $table->longText('transactions')->after('extended_status'); - $table->longText('errors')->after('transactions'); + $table->longText('transactions')->after('extended_status')->nullable(); + $table->longText('errors')->after('transactions')->nullable(); $table->integer('tag_id', false, true)->nullable()->after('user_id'); $table->foreign('tag_id')->references('id')->on('tags')->onDelete('set null'); diff --git a/public/js/ff/import/status_v2.js b/public/js/ff/import/status_v2.js index 512aaf5ccb..569944abf5 100644 --- a/public/js/ff/import/status_v2.js +++ b/public/js/ff/import/status_v2.js @@ -21,8 +21,8 @@ /** global: jobStatusUri */ var timeOutId; -var hasStartedJob = false; -var jobStorageStarted = false; +var jobRunRoutineStarted = false; +var jobStorageRoutineStarted = false; var checkInitialInterval = 1000; var checkNextInterval = 500; var maxLoops = 60; @@ -53,18 +53,12 @@ function reportJobJSONDone(data) { switch (data.status) { case "ready_to_run": if (startCount > 0) { - hasStartedJob = false; + jobRunRoutineStarted = false; } startCount++; sendJobPOSTStart(); recheckJobJSONStatus(); break; - case "running": - case "storing_data": - showProgressBox(data.status); - recheckJobJSONStatus(); - break; - case "need_job_config": // redirect user to configuration for this job. window.location.replace(jobConfigurationUri); @@ -74,11 +68,14 @@ function reportJobJSONDone(data) { sendJobPOSTStore(); recheckJobJSONStatus(); break; + case "storage_finished": case "finished": showJobResults(data); break; default: - console.error('Cannot handle status ' + data.status); + console.warn('No specific action for status ' + data.status); + showProgressBox(data.status); + recheckJobJSONStatus(); } } @@ -129,12 +126,12 @@ function recheckJobJSONStatus() { */ function sendJobPOSTStart() { console.log('In sendJobPOSTStart()'); - if (hasStartedJob) { + if (jobRunRoutineStarted) { console.log('Import job already started!'); return; } console.log('Job was started'); - hasStartedJob = true; + jobRunRoutineStarted = true; $.post(jobStartUri, {_token: token}).fail(reportJobPOSTFailure).done(reportJobPOSTDone) } @@ -143,12 +140,12 @@ function sendJobPOSTStart() { */ function sendJobPOSTStore() { console.log('In sendJobPOSTStore()'); - if (jobStorageStarted) { + if (jobStorageRoutineStarted) { console.log('Store job already started!'); return; } console.log('Storage job has started!'); - jobStorageStarted = true; + jobStorageRoutineStarted = true; $.post(jobStorageStartUri, {_token: token}).fail(reportJobPOSTFailure).done(reportJobPOSTDone) } @@ -185,14 +182,26 @@ function showProgressBox(status) { // hide initial status box: $('.status_initial').hide(); - if (status === 'running' || status === 'ready_to_run') { - $('#import-status-txt').text(langImportRunning); - } else { - $('#import-status-txt').text(langImportStoring); - } // show running box: $('.status_running').show(); + + if (status === 'running' || status === 'ready_to_run') { + $('#import-status-txt').text(langImportRunning); + return; + } + if (status === 'storing_data' || status === 'storage_finished' || status === 'stored_data') { + $('#import-status-txt').text(langImportStoring); + return; + } + if (status === 'applying_rules' || status === 'linking_to_tag' || status === 'linked_to_tag' || status === 'rules_applied') { + $('#import-status-txt').text(langImportRules); + return; + } + + $('#import-status-txt').text('Job status: ' + status); + + } /** diff --git a/resources/lang/en_US/import.php b/resources/lang/en_US/import.php index 6eb7ad6bdb..66b29a4541 100644 --- a/resources/lang/en_US/import.php +++ b/resources/lang/en_US/import.php @@ -79,7 +79,7 @@ return [ 'job_config_fake_song_title' => 'Enter song name', 'job_config_fake_song_text' => 'Mention the song "Golden years" to continue with the fake import.', 'job_config_fake_album_title' => 'Enter album name', - 'job_config_fake_album_text' => 'Some import routines require extra data halfway through the import. In the case of the fake import provider, you must answer some weird questions. In this case, enter "station to station" to continue.', + 'job_config_fake_album_text' => 'Some import routines require extra data halfway through the import. In the case of the fake import provider, you must answer some weird questions. Enter "Station to station" to continue.', // import status page: 'import_with_key' => 'Import with key \':key\'', @@ -88,6 +88,7 @@ return [ 'status_running_title' => 'The import is running', 'status_job_running' => 'Please wait, running the import...', 'status_job_storing' => 'Please wait, storing data...', + 'status_job_rules' => 'Please wait, running rules...', 'status_fatal_title' => 'Fatal error', 'status_fatal_text' => 'The import has suffered from an error it could not recover from. Apologies!', 'status_fatal_more' => 'This (possibly very cryptic) error message is complemented by log files, which you can find on your hard drive, or in the Docker container where you run Firefly III from.', diff --git a/resources/views/import/status.twig b/resources/views/import/status.twig index 6272a60ecd..4cc0744180 100644 --- a/resources/views/import/status.twig +++ b/resources/views/import/status.twig @@ -178,6 +178,7 @@ // import is running: var langImportRunning = '{{ trans('import.status_job_running') }}'; var langImportStoring = '{{ trans('import.status_job_storing') }}'; + var langImportRules = '{{ trans('import.status_job_rules') }}'; // some useful translations. {#var langImportTimeOutError = '(time out thing)';#} diff --git a/tests/Feature/Controllers/ExportControllerTest.php b/tests/Feature/Controllers/ExportControllerTest.php index 8d44b7f70c..43fd833e5b 100644 --- a/tests/Feature/Controllers/ExportControllerTest.php +++ b/tests/Feature/Controllers/ExportControllerTest.php @@ -116,7 +116,7 @@ class ExportControllerTest extends TestCase $journalRepos->shouldReceive('firstNull')->once()->andReturn(new TransactionJournal); $repository->shouldReceive('create')->andReturn($job); $repository->shouldReceive('cleanup'); - $accountRepos->shouldReceive('getAccountsByType')->withArgs([[AccountType::DEFAULT, AccountType::ASSET]])->andReturn(new Collection); + $accountRepos->shouldReceive('getAccountsByType')->withArgs([[AccountType::ASSET, AccountType::DEFAULT]])->andReturn(new Collection); $this->be($this->user()); $response = $this->get(route('export.index')); diff --git a/tests/Feature/Controllers/Import/ConfigurationControllerTest.php b/tests/Feature/Controllers/Import/ConfigurationControllerTest.php deleted file mode 100644 index a8f7a9b78a..0000000000 --- a/tests/Feature/Controllers/Import/ConfigurationControllerTest.php +++ /dev/null @@ -1,131 +0,0 @@ -. - */ -declare(strict_types=1); - -namespace Tests\Feature\Controllers\Import; - -use FireflyIII\Import\Configuration\FileConfigurator; -use FireflyIII\Models\ImportJob; -use FireflyIII\Repositories\ImportJob\ImportJobRepositoryInterface; -use Log; -use Tests\TestCase; - -/** - * Class AccountControllerTest - * - * @SuppressWarnings(PHPMD.TooManyPublicMethods) - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ -class ConfigurationControllerTest extends TestCase -{ - /** - * - */ - public function setUp() - { - parent::setUp(); - Log::debug(sprintf('Now in %s.', \get_class($this))); - } - - /** - * @covers \FireflyIII\Http\Controllers\Import\ConfigurationController::__construct - * @covers \FireflyIII\Http\Controllers\Import\ConfigurationController::index - * @covers \FireflyIII\Http\Controllers\Import\ConfigurationController::makeConfigurator - */ - public function testIndex() - { - /** @var ImportJob $job */ - $job = $this->user()->importJobs()->where('key', 'configuring')->first(); - $configurator = $this->mock(FileConfigurator::class); - $repository = $this->mock(ImportJobRepositoryInterface::class); - $configurator->shouldReceive('setJob')->once(); - $configurator->shouldReceive('isJobConfigured')->once()->andReturn(false); - $configurator->shouldReceive('getNextView')->once()->andReturn('error'); // does not matter which view is returned. - $configurator->shouldReceive('getNextData')->once()->andReturn([]); - $repository->shouldReceive('updateStatus')->once(); - - $this->be($this->user()); - $response = $this->get(route('import.configure', [$job->key])); - $response->assertStatus(200); - } - - /** - * @covers \FireflyIII\Http\Controllers\Import\ConfigurationController::__construct - * @covers \FireflyIII\Http\Controllers\Import\ConfigurationController::index - * @covers \FireflyIII\Http\Controllers\Import\ConfigurationController::makeConfigurator - */ - public function testIndexConfigured() - { - /** @var ImportJob $job */ - $job = $this->user()->importJobs()->where('key', 'configured')->first(); - $configurator = $this->mock(FileConfigurator::class); - $repository = $this->mock(ImportJobRepositoryInterface::class); - $configurator->shouldReceive('setJob')->once(); - $configurator->shouldReceive('isJobConfigured')->once()->andReturn(true); - $repository->shouldReceive('updateStatus')->once(); - - $this->be($this->user()); - $response = $this->get(route('import.configure', [$job->key])); - $response->assertStatus(302); - $response->assertRedirect(route('import.status', [$job->key])); - } - - /** - * @covers \FireflyIII\Http\Controllers\Import\ConfigurationController::post - */ - public function testPost() - { - /** @var ImportJob $job */ - $job = $this->user()->importJobs()->where('key', 'configuring')->first(); - $data = ['some' => 'config']; - $configurator = $this->mock(FileConfigurator::class); - $repository = $this->mock(ImportJobRepositoryInterface::class); - $configurator->shouldReceive('setJob')->once(); - $configurator->shouldReceive('isJobConfigured')->once()->andReturn(false); - $configurator->shouldReceive('configureJob')->once()->withArgs([$data]); - $configurator->shouldReceive('getWarningMessage')->once()->andReturn('Some warning'); - - $this->be($this->user()); - $response = $this->post(route('import.configure.post', [$job->key]), $data); - $response->assertStatus(302); - $response->assertRedirect(route('import.configure', [$job->key])); - } - - /** - * @covers \FireflyIII\Http\Controllers\Import\ConfigurationController::post - */ - public function testPostConfigured() - { - /** @var ImportJob $job */ - $job = $this->user()->importJobs()->where('key', 'configuring')->first(); - $data = ['some' => 'config']; - $configurator = $this->mock(FileConfigurator::class); - $repository = $this->mock(ImportJobRepositoryInterface::class); - $configurator->shouldReceive('setJob')->once(); - $configurator->shouldReceive('isJobConfigured')->once()->andReturn(true); - - $this->be($this->user()); - $response = $this->post(route('import.configure.post', [$job->key]), $data); - $response->assertStatus(302); - $response->assertRedirect(route('import.status', [$job->key])); - } -} diff --git a/tests/Feature/Controllers/Import/IndexControllerTest.php b/tests/Feature/Controllers/Import/IndexControllerTest.php index 9b24a5d9b3..0632960131 100644 --- a/tests/Feature/Controllers/Import/IndexControllerTest.php +++ b/tests/Feature/Controllers/Import/IndexControllerTest.php @@ -22,13 +22,18 @@ declare(strict_types=1); namespace Tests\Feature\Controllers\Import; -use FireflyIII\Import\Routine\FileRoutine; +use FireflyIII\Import\Prerequisites\BunqPrerequisites; +use FireflyIII\Import\Prerequisites\FakePrerequisites; +use FireflyIII\Import\Prerequisites\FilePrerequisites; +use FireflyIII\Import\Prerequisites\SpectrePrerequisites; +use FireflyIII\Models\ImportJob; use FireflyIII\Repositories\ImportJob\ImportJobRepositoryInterface; use Log; +use Mockery; use Tests\TestCase; /** - * Class AccountControllerTest + * Class IndexControllerTest * * @SuppressWarnings(PHPMD.TooManyPublicMethods) * @SuppressWarnings(PHPMD.ExcessiveMethodLength) @@ -46,73 +51,85 @@ class IndexControllerTest extends TestCase } /** - * @covers \FireflyIII\Http\Controllers\Import\IndexController::create + * @covers \FireflyIII\Http\Controllers\Import\IndexController */ - public function testCreate() + public function testCreateFake() { - $job = $this->user()->importJobs()->where('key', 'new')->first(); - $repository = $this->mock(ImportJobRepositoryInterface::class); - $repository->shouldReceive('create')->withArgs(['file'])->andReturn($job); + // mock stuff: + $repository = $this->mock(ImportJobRepositoryInterface::class); + $fakePrerequisites = $this->mock(FakePrerequisites::class); + + // fake job: + $importJob = new ImportJob; + $importJob->provider = 'fake'; + $importJob->key = 'fake_job_1'; + + // mock call: + $repository->shouldReceive('create')->withArgs(['fake'])->andReturn($importJob); + $fakePrerequisites->shouldReceive('isComplete')->once()->andReturn(false); + $fakePrerequisites->shouldReceive('setUser')->once(); + + $this->be($this->user()); - $response = $this->get(route('import.create-job', ['file'])); + $response = $this->get(route('import.create', ['fake'])); $response->assertStatus(302); - $response->assertRedirect(route('import.configure', ['new'])); - + // expect a redirect to prerequisites + $response->assertRedirect(route('import.prerequisites.index', ['fake', 'fake_job_1'])); } + /** - * @covers \FireflyIII\Http\Controllers\Import\IndexController::download + * @covers \FireflyIII\Http\Controllers\Import\IndexController */ - public function testDownload() + public function testCreateFakeNoPrereq() { - $repository = $this->mock(ImportJobRepositoryInterface::class); - //$job = $this->user()->importJobs()->where('key', 'testImport')->first(); + // mock stuff: + $repository = $this->mock(ImportJobRepositoryInterface::class); + $fakePrerequisites = $this->mock(FakePrerequisites::class); + + // fake job: + $importJob = new ImportJob; + $importJob->provider = 'fake'; + $importJob->key = 'fake_job_2'; + + // mock call: + $repository->shouldReceive('create')->withArgs(['fake'])->andReturn($importJob); + $fakePrerequisites->shouldReceive('isComplete')->once()->andReturn(true); + $fakePrerequisites->shouldReceive('setUser')->once(); + $repository->shouldReceive('setStatus')->withArgs([Mockery::any(), 'has_prereq'])->andReturn($importJob)->once(); + + $this->be($this->user()); - $response = $this->get(route('import.download', ['testImport'])); - $response->assertStatus(200); + $response = $this->get(route('import.create', ['fake'])); + $response->assertStatus(302); + // expect a redirect to prerequisites + $response->assertRedirect(route('import.job.configuration.index', ['fake_job_2'])); } - /** - * @covers \FireflyIII\Http\Controllers\Import\IndexController::__construct - * @covers \FireflyIII\Http\Controllers\Import\IndexController::index - */ public function testIndex() { - $repository = $this->mock(ImportJobRepositoryInterface::class); $this->be($this->user()); + + // fake prerequisites providers: + $fake = $this->mock(FakePrerequisites::class); + $file = $this->mock(FilePrerequisites::class); + $bunq = $this->mock(BunqPrerequisites::class); + $spectre = $this->mock(SpectrePrerequisites::class); + + // call methods: + $fake->shouldReceive('setUser')->once(); + $file->shouldReceive('setUser')->once(); + $bunq->shouldReceive('setUser')->once(); + $spectre->shouldReceive('setUser')->once(); + + $fake->shouldReceive('isComplete')->once()->andReturn(true); + $file->shouldReceive('isComplete')->once()->andReturn(true); + $bunq->shouldReceive('isComplete')->once()->andReturn(true); + $spectre->shouldReceive('isComplete')->once()->andReturn(true); + + $response = $this->get(route('import.index')); $response->assertStatus(200); - - } - - /** - * @covers \FireflyIII\Http\Controllers\Import\IndexController::start - */ - public function testStart() - { - $repository = $this->mock(ImportJobRepositoryInterface::class); - $routine = $this->mock(FileRoutine::class); - $routine->shouldReceive('setJob')->once(); - $routine->shouldReceive('run')->once()->andReturn(true); - - $this->be($this->user()); - $response = $this->post(route('import.start', ['configured'])); - $response->assertStatus(200); - } - - /** - * @covers \FireflyIII\Http\Controllers\Import\IndexController::start - * @expectedExceptionMessage Job did not complete successfully. - */ - public function testStartFailed() - { - $repository = $this->mock(ImportJobRepositoryInterface::class); - $routine = $this->mock(FileRoutine::class); - $routine->shouldReceive('setJob')->once(); - $routine->shouldReceive('run')->once()->andReturn(false); - - $this->be($this->user()); - $response = $this->post(route('import.start', ['configured'])); - $response->assertStatus(500); + $response->assertSee('