diff --git a/app/Import/Configuration/BunqConfigurator.php b/app/Import/Configuration/BunqConfigurator.php index ceca16f03e..27604e46b5 100644 --- a/app/Import/Configuration/BunqConfigurator.php +++ b/app/Import/Configuration/BunqConfigurator.php @@ -25,7 +25,8 @@ namespace FireflyIII\Import\Configuration; use FireflyIII\Exceptions\FireflyException; use FireflyIII\Models\ImportJob; use FireflyIII\Repositories\ImportJob\ImportJobRepositoryInterface; -use FireflyIII\Support\Import\Configuration\Spectre\HaveAccounts; +use FireflyIII\Support\Import\Configuration\Bunq\HasAccounts; +use FireflyIII\Support\Import\Configuration\Bunq\HaveAccounts; use Log; /** @@ -65,8 +66,6 @@ class BunqConfigurator implements ConfiguratorInterface $stage = $this->getConfig()['stage'] ?? 'initial'; Log::debug(sprintf('in getNextData(), for stage "%s".', $stage)); - throw new FireflyException('configureJob: Will try to handle stage ' . $stage); - switch ($stage) { case 'have-accounts': /** @var HaveAccounts $class */ @@ -102,20 +101,7 @@ class BunqConfigurator implements ConfiguratorInterface Log::debug(sprintf('in getNextData(), for stage "%s".', $stage)); - throw new FireflyException('getNextData: Will try to handle stage ' . $stage); - switch ($stage) { - case 'has-token': - // simply redirect to Spectre. - $config['is-redirected'] = true; - $config['stage'] = 'user-logged-in'; - $status = 'configured'; - - // update config and status: - $this->repository->setConfiguration($this->job, $config); - $this->repository->setStatus($this->job, $status); - - return $this->repository->getConfiguration($this->job); case 'have-accounts': /** @var HaveAccounts $class */ $class = app(HaveAccounts::class); @@ -139,17 +125,10 @@ class BunqConfigurator implements ConfiguratorInterface } $stage = $this->getConfig()['stage'] ?? 'initial'; - throw new FireflyException('Will try to handle stage ' . $stage); - Log::debug(sprintf('getNextView: in getNextView(), for stage "%s".', $stage)); switch ($stage) { - case 'has-token': - // redirect to Spectre. - Log::info('User is being redirected to Spectre.'); - - return 'import.spectre.redirect'; case 'have-accounts': - return 'import.spectre.accounts'; + return 'import.bunq.accounts'; default: return ''; @@ -179,16 +158,10 @@ class BunqConfigurator implements ConfiguratorInterface Log::debug(sprintf('in isJobConfigured(), for stage "%s".', $stage)); switch ($stage) { - case 'has-token': case 'have-accounts': Log::debug('isJobConfigured returns false'); return false; - case 'initial': - Log::debug('isJobConfigured returns true'); - - return true; - break; default: Log::debug('isJobConfigured returns true'); diff --git a/app/Import/Routine/BunqRoutine.php b/app/Import/Routine/BunqRoutine.php index 0564dd860f..df857edf49 100644 --- a/app/Import/Routine/BunqRoutine.php +++ b/app/Import/Routine/BunqRoutine.php @@ -29,12 +29,18 @@ use FireflyIII\Models\ImportJob; use FireflyIII\Repositories\ImportJob\ImportJobRepositoryInterface; use FireflyIII\Services\Bunq\Id\DeviceServerId; use FireflyIII\Services\Bunq\Object\DeviceServer; +use FireflyIII\Services\Bunq\Object\MonetaryAccountBank; use FireflyIII\Services\Bunq\Object\ServerPublicKey; +use FireflyIII\Services\Bunq\Object\UserCompany; +use FireflyIII\Services\Bunq\Object\UserPerson; use FireflyIII\Services\Bunq\Request\DeviceServerRequest; use FireflyIII\Services\Bunq\Request\DeviceSessionRequest; use FireflyIII\Services\Bunq\Request\InstallationTokenRequest; use FireflyIII\Services\Bunq\Request\ListDeviceServerRequest; +use FireflyIII\Services\Bunq\Request\ListMonetaryAccountRequest; +use FireflyIII\Services\Bunq\Request\ListPaymentRequest; use FireflyIII\Services\Bunq\Token\InstallationToken; +use FireflyIII\Services\Bunq\Token\SessionToken; use Illuminate\Support\Collection; use Log; use Preferences; @@ -60,9 +66,11 @@ use Requests; * * Get list of bank accounts * - * Stage 'has-accounts' + * Stage 'have-accounts' * - * Do customer statement export (in CSV?) + * Map accounts to existing accounts + * + * Stage 'do-import'? * * */ @@ -122,19 +130,57 @@ class BunqRoutine implements RoutineInterface { Log::info(sprintf('Start with import job %s using Bunq.', $this->job->key)); set_time_limit(0); - // check if job has token first! - $stage = $this->getConfig()['stage'] ?? 'unknown'; + // 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->repository->setUser($job->user); + } + + /** + * @throws FireflyException + */ + protected function continueJob() + { + // if in "configuring" + if ($this->getStatus() === 'configuring') { + 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->runStageHaveAccountMapping(); + break; default: throw new FireflyException(sprintf('No action for stage %s!', $stage)); break; @@ -154,18 +200,6 @@ class BunqRoutine implements RoutineInterface // // return true; } - - return true; - } - - /** - * @param ImportJob $job - */ - public function setJob(ImportJob $job) - { - $this->job = $job; - $this->repository = app(ImportJobRepositoryInterface::class); - $this->repository->setUser($job->user); } /** @@ -206,18 +240,22 @@ class BunqRoutine implements RoutineInterface $request->setServerPublicKey($serverPublicKey); $request->setSecret($apiKey); $request->call(); + $this->addStep(); // todo store objects in job! $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(); return; } @@ -524,6 +562,95 @@ class BunqRoutine implements RoutineInterface 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; + if ($user->getId() === 0) { + $user = new UserCompany($config['user_company']); + } + + foreach ($config['accounts'] as $accountData) { + $account = new MonetaryAccountBank($accountData); + $importId = $account->getId(); + if ($mapping[$importId] === 1) { + // grab all transactions + $request = new ListPaymentRequest(); + $request->setPrivateKey($this->getPrivateKey()); + $request->setServerPublicKey($this->getServerPublicKey()); + $request->setSessionToken($token); + $request->setUserId($user->getId()); + $request->setAccount($account); + $request->call(); + + exit; + + // store in array + $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); + } + exit; + } + + /** + * + * @throws FireflyException + */ + private function runStageLoggedIn(): void + { + // grab new session token: + $config = $this->getConfig(); + $token = new SessionToken($config['session_token']); + $user = new UserPerson($config['user_person']); + if ($user->getId() === 0) { + $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())); + + /** @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'); + + return; + } + /** * Shorthand. * diff --git a/app/Services/Bunq/Object/Alias.php b/app/Services/Bunq/Object/Alias.php index 7457eb0cda..a0d3877f06 100644 --- a/app/Services/Bunq/Object/Alias.php +++ b/app/Services/Bunq/Object/Alias.php @@ -71,4 +71,16 @@ class Alias extends BunqObject { return $this->value; } + + /** + * @return array + */ + public function toArray(): array + { + return [ + 'type' => $this->type, + 'name' => $this->name, + 'value' => $this->value, + ]; + } } diff --git a/app/Services/Bunq/Object/Amount.php b/app/Services/Bunq/Object/Amount.php index 2875472c69..f142424d8f 100644 --- a/app/Services/Bunq/Object/Amount.php +++ b/app/Services/Bunq/Object/Amount.php @@ -60,4 +60,15 @@ class Amount extends BunqObject { return $this->value; } + + /** + * @return array + */ + public function toArray(): array + { + return [ + 'currency' => $this->currency, + 'value' => $this->value, + ]; + } } diff --git a/app/Services/Bunq/Object/LabelMonetaryAccount.php b/app/Services/Bunq/Object/LabelMonetaryAccount.php new file mode 100644 index 0000000000..03c53745b5 --- /dev/null +++ b/app/Services/Bunq/Object/LabelMonetaryAccount.php @@ -0,0 +1,33 @@ +. + */ + +declare(strict_types=1); + +namespace FireflyIII\Services\Bunq\Object; + + +/** + * Class LabelMonetaryAccount + */ +class LabelMonetaryAccount extends BunqObject +{ + +} \ No newline at end of file diff --git a/app/Services/Bunq/Object/MonetaryAccountBank.php b/app/Services/Bunq/Object/MonetaryAccountBank.php index 092572b455..619a8827d9 100644 --- a/app/Services/Bunq/Object/MonetaryAccountBank.php +++ b/app/Services/Bunq/Object/MonetaryAccountBank.php @@ -89,14 +89,10 @@ class MonetaryAccountBank extends BunqObject $this->status = $data['status']; $this->subStatus = $data['sub_status']; $this->userId = $data['user_id']; - $this->status = $data['status']; - $this->subStatus = $data['sub_status']; $this->monetaryAccountProfile = new MonetaryAccountProfile($data['monetary_account_profile']); $this->setting = new MonetaryAccountSetting($data['setting']); $this->overdraftLimit = new Amount($data['overdraft_limit']); - $this->publicUuid = $data['public_uuid']; - // create aliases: foreach ($data['alias'] as $alias) { $this->aliases[] = new Alias($alias); @@ -106,6 +102,10 @@ class MonetaryAccountBank extends BunqObject $this->notificationFilters[] = new NotificationFilter($filter); } + // TODO avatar + // TODO reason + // TODO reason description + return; } @@ -156,4 +156,46 @@ class MonetaryAccountBank extends BunqObject { return $this->setting; } + + /** + * @return array + */ + public function toArray(): array + { + $data = [ + 'id' => $this->id, + 'created' => $this->created->format('Y-m-d H:i:s.u'), + 'updated' => $this->updated->format('Y-m-d H:i:s.u'), + 'balance' => $this->balance->toArray(), + 'currency' => $this->currency, + 'daily_limit' => $this->dailyLimit->toArray(), + 'daily_spent' => $this->dailySpent->toArray(), + 'description' => $this->description, + 'public_uuid' => $this->publicUuid, + 'status' => $this->status, + 'sub_status' => $this->subStatus, + 'user_id' => $this->userId, + 'monetary_account_profile' => $this->monetaryAccountProfile->toArray(), + 'setting' => $this->setting->toArray(), + 'overdraft_limit' => $this->overdraftLimit->toArray(), + 'alias' => [], + 'notification_filters' => [], + ]; + + /** @var Alias $alias */ + foreach ($this->aliases as $alias) { + $data['alias'][] = $alias->toArray(); + } + + /** @var NotificationFilter $filter */ + foreach ($this->notificationFilters as $filter) { + $data['notification_filters'][] = $filter->toArray(); + } + + // TODO avatar + // TODO reason + // TODO reason description + + return $data; + } } diff --git a/app/Services/Bunq/Object/MonetaryAccountProfile.php b/app/Services/Bunq/Object/MonetaryAccountProfile.php index 1de1fb4099..c51223671b 100644 --- a/app/Services/Bunq/Object/MonetaryAccountProfile.php +++ b/app/Services/Bunq/Object/MonetaryAccountProfile.php @@ -54,4 +54,15 @@ class MonetaryAccountProfile extends BunqObject return; } + + /** + * @return array + */ + public function toArray(): array + { + return [ + 'profile_action_required' => $this->profileActionRequired, + 'profile_amount_required' => $this->profileAmountRequired->toArray(), + ]; + } } diff --git a/app/Services/Bunq/Object/MonetaryAccountSetting.php b/app/Services/Bunq/Object/MonetaryAccountSetting.php index ac189a7477..2fb81df801 100644 --- a/app/Services/Bunq/Object/MonetaryAccountSetting.php +++ b/app/Services/Bunq/Object/MonetaryAccountSetting.php @@ -71,4 +71,16 @@ class MonetaryAccountSetting extends BunqObject { return $this->restrictionChat; } + + /** + * @return array + */ + public function toArray(): array + { + return [ + 'color' => $this->color, + 'default_avatar_status' => $this->defaultAvatarStatus, + 'restriction_chat' => $this->restrictionChat, + ]; + } } diff --git a/app/Services/Bunq/Object/NotificationFilter.php b/app/Services/Bunq/Object/NotificationFilter.php index 11ec026496..b61dbf0664 100644 --- a/app/Services/Bunq/Object/NotificationFilter.php +++ b/app/Services/Bunq/Object/NotificationFilter.php @@ -36,4 +36,12 @@ class NotificationFilter extends BunqObject { unset($data); } + + /** + * @return array + */ + public function toArray(): array + { + return []; + } } diff --git a/app/Services/Bunq/Object/Payment.php b/app/Services/Bunq/Object/Payment.php new file mode 100644 index 0000000000..f94545afa9 --- /dev/null +++ b/app/Services/Bunq/Object/Payment.php @@ -0,0 +1,70 @@ +. + */ + +declare(strict_types=1); + +namespace FireflyIII\Services\Bunq\Object; +use Carbon\Carbon; + + +/** + * Class Payment + */ +class Payment extends BunqObject +{ + /** @var int */ + private $id; + /** @var Carbon */ + private $created; + /** @var Carbon */ + private $updated; + /** @var int */ + private $monetaryAccountId; + /** @var Amount */ + private $amount; + /** @var string */ + private $description; + /** @var string */ + private $type; + /** @var string */ + private $merchantReference; + /** @var LabelMonetaryAccount */ + private $counterParty; + /** @var array */ + private $attachments = []; + /** @var string */ + private $subType; + + /** + * Payment constructor. + * + * @param array $data + */ + public function __construct(array $data) + { + $this->id = $data['id']; + $this->created = new Carbon(); + + var_dump($data); + exit; + } + +} \ No newline at end of file diff --git a/app/Services/Bunq/Request/ListMonetaryAccountRequest.php b/app/Services/Bunq/Request/ListMonetaryAccountRequest.php index 761e203b86..d51df9d792 100644 --- a/app/Services/Bunq/Request/ListMonetaryAccountRequest.php +++ b/app/Services/Bunq/Request/ListMonetaryAccountRequest.php @@ -39,7 +39,7 @@ class ListMonetaryAccountRequest extends BunqRequest private $userId = 0; /** - * @throws \Exception + * @throws \FireflyIII\Exceptions\FireflyException */ public function call(): void { diff --git a/app/Services/Bunq/Request/ListPaymentRequest.php b/app/Services/Bunq/Request/ListPaymentRequest.php new file mode 100644 index 0000000000..ed5b99d978 --- /dev/null +++ b/app/Services/Bunq/Request/ListPaymentRequest.php @@ -0,0 +1,95 @@ +. + */ + +declare(strict_types=1); + +namespace FireflyIII\Services\Bunq\Request; + +use FireflyIII\Services\Bunq\Object\MonetaryAccountBank; +use FireflyIII\Services\Bunq\Object\Payment; +use FireflyIII\Services\Bunq\Token\SessionToken; +use Illuminate\Support\Collection; + + +/** + * Class ListPaymentRequest + */ +class ListPaymentRequest extends BunqRequest +{ + + /** @var MonetaryAccountBank */ + private $account; + /** @var Collection */ + private $payments; + /** @var SessionToken */ + private $sessionToken; + /** @var int */ + private $userId = 0; + + /** + * TODO support pagination. + * + * @throws \FireflyIII\Exceptions\FireflyException + */ + public function call(): void + { + $this->payments = new Collection; + $uri = sprintf('user/%d/monetary-account/%d/payment', $this->userId, $this->account->getId()); + $data = []; + $headers = $this->getDefaultHeaders(); + $headers['X-Bunq-Client-Authentication'] = $this->sessionToken->getToken(); + $response = $this->sendSignedBunqGet($uri, $data, $headers); + + // create payment objects: + $raw = $this->getArrayFromResponse('Payment', $response); + foreach ($raw as $entry) { + $account = new Payment($entry); + $this->payments->push($account); + } + + return; + + } + + /** + * @param MonetaryAccountBank $account + */ + public function setAccount(MonetaryAccountBank $account): void + { + $this->account = $account; + } + + /** + * @param SessionToken $sessionToken + */ + public function setSessionToken(SessionToken $sessionToken): void + { + $this->sessionToken = $sessionToken; + } + + /** + * @param int $userId + */ + public function setUserId(int $userId): void + { + $this->userId = $userId; + } +} \ No newline at end of file diff --git a/app/Support/Import/Configuration/Bunq/HaveAccounts.php b/app/Support/Import/Configuration/Bunq/HaveAccounts.php new file mode 100644 index 0000000000..8dc16ccf7e --- /dev/null +++ b/app/Support/Import/Configuration/Bunq/HaveAccounts.php @@ -0,0 +1,154 @@ +. + */ + +declare(strict_types=1); + +namespace FireflyIII\Support\Import\Configuration\Bunq; + +use FireflyIII\Models\Account; +use FireflyIII\Models\AccountType; +use FireflyIII\Models\ImportJob; +use FireflyIII\Repositories\Account\AccountRepositoryInterface; +use FireflyIII\Repositories\Currency\CurrencyRepositoryInterface; +use FireflyIII\Support\Import\Configuration\ConfigurationInterface; +use Illuminate\Support\Collection; + +/** + * Class HaveAccounts + */ +class HaveAccounts implements ConfigurationInterface +{ + /** @var ImportJob */ + private $job; + + /** + * Get the data necessary to show the configuration screen. + * + * @return array + */ + public function getData(): array + { + /** @var AccountRepositoryInterface $accountRepository */ + $accountRepository = app(AccountRepositoryInterface::class); + /** @var CurrencyRepositoryInterface $currencyRepository */ + $currencyRepository = app(CurrencyRepositoryInterface::class); + $config = $this->job->configuration; + $collection = $accountRepository->getAccountsByType([AccountType::DEFAULT, AccountType::ASSET]); + $defaultCurrency = app('amount')->getDefaultCurrency(); + $dbAccounts = []; + /** @var Account $dbAccount */ + foreach ($collection as $dbAccount) { + $id = $dbAccount->id; + $currencyId = intval($accountRepository->getMetaValue($dbAccount, 'currency_id')); + $currency = $currencyRepository->findNull($currencyId); + $dbAccounts[$id] = [ + 'account' => $dbAccount, + 'currency' => is_null($currency) ? $defaultCurrency : $currency, + ]; + } + + // loop Bunq accounts: + /** + * @var int $index + * @var array $bunqAccount + */ + foreach ($config['accounts'] as $index => $bunqAccount) { + // find accounts with currency code + $code = $bunqAccount['currency']; + $selection = $this->filterAccounts($dbAccounts, $code); + $config['accounts'][$index]['options'] = app('expandedform')->makeSelectList($selection); + } + + + $data = [ + 'config' => $config, + ]; + + return $data; + } + + /** + * Return possible warning to user. + * + * @return string + */ + public function getWarningMessage(): string + { + return ''; + } + + /** + * @param ImportJob $job + * + * @return ConfigurationInterface + */ + public function setJob(ImportJob $job) + { + $this->job = $job; + + return $this; + } + + /** + * Store the result. + * + * @param array $data + * + * @return bool + */ + public function storeConfiguration(array $data): bool + { + $accounts = $data['bunq_account_id'] ?? []; + $mapping = []; + foreach ($accounts as $bunqId) { + $bunqId = intval($bunqId); + $doImport = intval($data['do_import'][$bunqId] ?? 0) === 1; + $account = intval($data['import'][$bunqId] ?? 0); + if ($doImport) { + $mapping[$bunqId] = $account; + } + } + $config = $this->job->configuration; + $config['accounts-mapped'] = $mapping; + $this->job->configuration = $config; + $this->job->save(); + + return true; + } + + /** + * @param array $dbAccounts + * @param string $code + * + * @return Collection + */ + private function filterAccounts(array $dbAccounts, string $code): Collection + { + $collection = new Collection; + foreach ($dbAccounts as $accountId => $data) { + if ($data['currency']->code === $code) { + $collection->push($data['account']); + } + } + + return $collection; + } +} diff --git a/resources/views/import/bunq/accounts.twig b/resources/views/import/bunq/accounts.twig new file mode 100644 index 0000000000..734de2893b --- /dev/null +++ b/resources/views/import/bunq/accounts.twig @@ -0,0 +1,83 @@ +{% extends "./layout/default" %} + +{% block breadcrumbs %} + {{ Breadcrumbs.render }} +{% endblock %} +{% block content %} +
+
+ +
+
+
+

{{ trans('import.bunq_accounts_title') }}

+
+
+
+
+

+ {{ trans('import.bunq_accounts_text')|raw }} +

+
+
+
+
+ + + + + + + + + + + {% for account in data.config.accounts %} + + + + + + + + {% endfor %} + +
 {{ trans('list.account_at_bunq') }}{{ trans('list.account') }}{{ trans('list.do_import') }}
+ + {{ account.description }}
+ {% for alias in account.aliases %} + {% if alias.type == 'IBAN' %} + {{ alias.name }}: {{ alias.value }} + {% endif %} + {% endfor %} +
+ + +
+ +
+ +
+
+
+ +
+
+ +
+{% endblock %} +{% block scripts %} +{% endblock %} +{% block styles %} +{% endblock %}