diff --git a/app/Import/Routine/SpectreRoutine.php b/app/Import/Routine/SpectreRoutine.php index fd9f0c6e03..fbd12ae9c7 100644 --- a/app/Import/Routine/SpectreRoutine.php +++ b/app/Import/Routine/SpectreRoutine.php @@ -22,11 +22,11 @@ declare(strict_types=1); namespace FireflyIII\Import\Routine; +use FireflyIII\Exceptions\FireflyException; use FireflyIII\Models\ImportJob; -use FireflyIII\Models\SpectreProvider; use FireflyIII\Services\Spectre\Object\Customer; -use FireflyIII\Services\Spectre\Request\CreateLoginRequest; -use FireflyIII\Services\Spectre\Request\ListLoginsRequest; +use FireflyIII\Services\Spectre\Object\Token; +use FireflyIII\Services\Spectre\Request\CreateTokenRequest; use FireflyIII\Services\Spectre\Request\NewCustomerRequest; use Illuminate\Support\Collection; use Log; @@ -90,31 +90,48 @@ class SpectreRoutine implements RoutineInterface return false; } - set_time_limit(0); Log::info(sprintf('Start with import job %s using Spectre.', $this->job->key)); - // create customer if user does not have one: - $customer = $this->getCustomer(); + set_time_limit(0); - // list all logins present at Spectre - $logins = $this->listLogins($customer); + // check if job has token first! + $config = $this->job->configuration; + $hasToken = $config['has-token'] ?? false; + if ($hasToken === false) { + Log::debug('Job has no token'); + // 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())); - // use latest (depending on status, and if login exists for selected country + provider) - $country = $this->job->configuration['country']; - $providerId = $this->job->configuration['provider']; - $login = $this->filterLogins($logins, $country, $providerId); + // update job, give it the token: + $config = $this->job->configuration; + $config['has-token'] = true; + $config['token'] = $token->getToken(); + $config['token-expires'] = $token->getExpiresAt()->format('U'); + $config['token-url'] = $token->getConnectUrl(); + $this->job->configuration = $config; - // create new login if list is empty or no login exists. - if (is_null($login)) { - $login = $this->createLogin($customer); - var_dump($login); - exit; + Log::debug('Job config is now', $config); + + // update job, set status to "configuring". + $this->job->status = 'configuring'; + $this->job->save(); + Log::debug(sprintf('Job status is now %s', $this->job->status)); + + return true; + } + $isRedirected = $config['is-redirected'] ?? false; + if ($isRedirected === true) { + // assume user has "used" the token. + // ... + // now what? + throw new FireflyException('Application cannot handle this.'); } - echo '
';
- print_r($logins);
- exit;
-
- return true;
+ throw new FireflyException('Application cannot handle this.');
}
/**
@@ -135,49 +152,12 @@ class SpectreRoutine implements RoutineInterface
$newCustomerRequest->call();
$customer = $newCustomerRequest->getCustomer();
- // store customer. Not sure where. User preference? TODO
+ Preferences::setForUser($this->job->user, 'spectre_customer', $customer->toArray());
+
return $customer;
}
- /**
- * @param Customer $customer
- */
- protected function createLogin(Customer $customer)
- {
-
- $providerId = intval($this->job->configuration['provider']);
- $provider = $this->findProvider($providerId);
-
-
- $createLoginRequest = new CreateLoginRequest($this->job->user);
- $createLoginRequest->setCustomer($customer);
- $createLoginRequest->setProvider($provider);
- $createLoginRequest->setMandatoryFields($this->decrypt($this->job->configuration['mandatory-fields']));
- $createLoginRequest->call();
- echo '123';
- // country code, provider code (find by spectre ID)
- // credentials
- // daily_refresh=true
- // fetch_type=recent
- // include_fake_providers=true
- // store_credentials=true
-
-
- var_dump($this->job->configuration);
- exit;
- }
-
- /**
- * @param int $providerId
- *
- * @return SpectreProvider|null
- */
- protected function findProvider(int $providerId): ?SpectreProvider
- {
- return SpectreProvider::where('spectre_id', $providerId)->first();
- }
-
/**
* @return Customer
* @throws \FireflyIII\Exceptions\FireflyException
@@ -188,61 +168,27 @@ class SpectreRoutine implements RoutineInterface
if (is_null($preference)) {
return $this->createCustomer();
}
- var_dump($preference->data);
- exit;
+ $customer = new Customer($preference->data);
+
+ return $customer;
}
/**
* @param Customer $customer
+ * @param string $returnUri
*
- * @return array
+ * @return Token
* @throws \FireflyIII\Exceptions\FireflyException
*/
- protected function listLogins(Customer $customer): array
+ protected function getToken(Customer $customer, string $returnUri): Token
{
- $listLoginRequest = new ListLoginsRequest($this->job->user);
- $listLoginRequest->setCustomer($customer);
- $listLoginRequest->call();
+ $request = new CreateTokenRequest($this->job->user);
+ $request->setUri($returnUri);
+ $request->setCustomer($customer);
+ $request->call();
+ Log::debug('Call to get token is finished');
- $logins = $listLoginRequest->getLogins();
+ return $request->getToken();
- return $logins;
- }
-
- /**
- * @param array $configuration
- *
- * @return array
- */
- private function decrypt(array $configuration): array
- {
- $new = [];
- foreach ($configuration as $key => $value) {
- $new[$key] = app('steam')->tryDecrypt($value);
- }
-
- return $new;
- }
-
- /**
- * Return login belonging to country and provider
- * TODO must return Login object, not array
- *
- * @param array $logins
- * @param string $country
- * @param int $providerId
- *
- * @return array|null
- */
- private function filterLogins(array $logins, string $country, int $providerId): ?array
- {
- if (count($logins) === 0) {
- return null;
- }
- foreach ($logins as $login) {
- die('do some filter');
- }
-
- return null;
}
}
diff --git a/app/Jobs/GetSpectreProviders.php b/app/Jobs/GetSpectreProviders.php
deleted file mode 100644
index d29da378fd..0000000000
--- a/app/Jobs/GetSpectreProviders.php
+++ /dev/null
@@ -1,109 +0,0 @@
-.
- */
-
-namespace FireflyIII\Jobs;
-
-use FireflyIII\Models\Configuration;
-use FireflyIII\Models\SpectreProvider;
-use FireflyIII\Services\Spectre\Request\ListProvidersRequest;
-use FireflyIII\User;
-use Illuminate\Bus\Queueable;
-use Illuminate\Contracts\Queue\ShouldQueue;
-use Illuminate\Foundation\Bus\Dispatchable;
-use Illuminate\Queue\InteractsWithQueue;
-use Illuminate\Queue\SerializesModels;
-use Log;
-
-/**
- * Class GetSpectreProviders
- */
-class GetSpectreProviders implements ShouldQueue
-{
- use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
-
- /**
- * @var User
- */
- protected $user;
-
- /**
- * Create a new job instance.
- *
- * @param User $user
- */
- public function __construct(User $user)
- {
- $this->user = $user;
- Log::debug('Constructed job GetSpectreProviders');
- }
-
- /**
- * Execute the job.
- *
- * @throws \Illuminate\Container\EntryNotFoundException
- * @throws \Exception
- */
- public function handle()
- {
- /** @var Configuration $configValue */
- $configValue = app('fireflyconfig')->get('spectre_provider_download', 0);
- $now = time();
- if ($now - intval($configValue->data) < 86400) {
- Log::debug(sprintf('Difference is %d, so will NOT execute job.', ($now - intval($configValue->data))));
-
- return;
- }
- Log::debug(sprintf('Difference is %d, so will execute job.', ($now - intval($configValue->data))));
-
- // get user
-
- // fire away!
- $request = new ListProvidersRequest($this->user);
- $request->call();
-
- // store all providers:
- $providers = $request->getProviders();
- foreach ($providers as $provider) {
- // find provider?
- $dbProvider = SpectreProvider::where('spectre_id', $provider['id'])->first();
- if (is_null($dbProvider)) {
- $dbProvider = new SpectreProvider;
- }
- // update fields:
- $dbProvider->spectre_id = $provider['id'];
- $dbProvider->code = $provider['code'];
- $dbProvider->mode = $provider['mode'];
- $dbProvider->status = $provider['status'];
- $dbProvider->interactive = 1 === $provider['interactive'];
- $dbProvider->automatic_fetch = 1 === $provider['automatic_fetch'];
- $dbProvider->country_code = $provider['country_code'];
- $dbProvider->data = $provider;
- $dbProvider->save();
- Log::debug(sprintf('Stored provider #%d under ID #%d', $provider['id'], $dbProvider->id));
- }
-
- app('fireflyconfig')->set('spectre_provider_download', time());
-
- return;
- }
-}
diff --git a/app/Models/ImportJob.php b/app/Models/ImportJob.php
index b9256ea25a..57015f946b 100644
--- a/app/Models/ImportJob.php
+++ b/app/Models/ImportJob.php
@@ -127,6 +127,7 @@ class ImportJob extends Model
public function change(string $status): void
{
if (in_array($status, $this->validStatus)) {
+ Log::debug(sprintf('Job status set (in model) to "%s"', $status));
$this->status = $status;
$this->save();
@@ -169,6 +170,7 @@ class ImportJob extends Model
/**
* @codeCoverageIgnore
+ *
* @param $value
*/
public function setConfigurationAttribute($value)
@@ -178,6 +180,7 @@ class ImportJob extends Model
/**
* @codeCoverageIgnore
+ *
* @param $value
*/
public function setExtendedStatusAttribute($value)
diff --git a/app/Models/SpectreProvider.php b/app/Models/SpectreProvider.php
deleted file mode 100644
index a1d056f93c..0000000000
--- a/app/Models/SpectreProvider.php
+++ /dev/null
@@ -1,52 +0,0 @@
-.
- */
-declare(strict_types=1);
-
-namespace FireflyIII\Models;
-
-use Illuminate\Database\Eloquent\Model;
-
-/**
- * Class SpectreProvider
- */
-class SpectreProvider extends Model
-{
- /**
- * The attributes that should be casted to native types.
- *
- * @var array
- */
- protected $casts
- = [
- 'spectre_id' => 'int',
- 'created_at' => 'datetime',
- 'updated_at' => 'datetime',
- 'deleted_at' => 'datetime',
- 'interactive' => 'boolean',
- 'automatic_fetch' => 'boolean',
- 'data' => 'array',
- ];
-
- /**
- * @var array
- */
- protected $fillable = ['spectre_id', 'code', 'mode', 'name', 'status', 'interactive', 'automatic_fetch', 'country_code', 'data'];
-}
diff --git a/app/Services/Spectre/Object/Customer.php b/app/Services/Spectre/Object/Customer.php
index 53d046a6d9..755dcfc38d 100644
--- a/app/Services/Spectre/Object/Customer.php
+++ b/app/Services/Spectre/Object/Customer.php
@@ -93,4 +93,16 @@ class Customer extends SpectreObject
{
$this->secret = $secret;
}
+
+ /**
+ * @return array
+ */
+ public function toArray(): array
+ {
+ return [
+ 'id' => $this->id,
+ 'identifier' => $this->identifier,
+ 'secret' => $this->secret,
+ ];
+ }
}
diff --git a/app/Services/Spectre/Object/Login.php b/app/Services/Spectre/Object/Login.php
deleted file mode 100644
index 6de8bed874..0000000000
--- a/app/Services/Spectre/Object/Login.php
+++ /dev/null
@@ -1,32 +0,0 @@
-.
- */
-
-declare(strict_types=1);
-
-namespace FireflyIII\Services\Spectre\Object;
-
-/**
- * Class Login
- */
-class Login extends SpectreObject
-{
-
-}
\ No newline at end of file
diff --git a/app/Services/Spectre/Object/Token.php b/app/Services/Spectre/Object/Token.php
new file mode 100644
index 0000000000..d2e51d3c18
--- /dev/null
+++ b/app/Services/Spectre/Object/Token.php
@@ -0,0 +1,76 @@
+.
+ */
+
+declare(strict_types=1);
+
+namespace FireflyIII\Services\Spectre\Object;
+
+use Carbon\Carbon;
+
+/**
+ * Class Token
+ */
+class Token extends SpectreObject
+{
+ /** @var string */
+ private $connectUrl;
+ /** @var Carbon */
+ private $expiresAt;
+ /** @var string */
+ private $token;
+
+ /**
+ * Token constructor.
+ *
+ * @param array $data
+ */
+ public function __construct(array $data)
+ {
+ $this->token = $data['token'];
+ $this->expiresAt = new Carbon($data['expires_at']);
+ $this->connectUrl = $data['connect_url'];
+ }
+
+ /**
+ * @return string
+ */
+ public function getConnectUrl(): string
+ {
+ return $this->connectUrl;
+ }
+
+ /**
+ * @return Carbon
+ */
+ public function getExpiresAt(): Carbon
+ {
+ return $this->expiresAt;
+ }
+
+ /**
+ * @return string
+ */
+ public function getToken(): string
+ {
+ return $this->token;
+ }
+
+}
\ No newline at end of file
diff --git a/app/Services/Spectre/Request/CreateLoginRequest.php b/app/Services/Spectre/Request/CreateLoginRequest.php
deleted file mode 100644
index 1b0e3e6b79..0000000000
--- a/app/Services/Spectre/Request/CreateLoginRequest.php
+++ /dev/null
@@ -1,109 +0,0 @@
-.
- */
-
-declare(strict_types=1);
-
-namespace FireflyIII\Services\Spectre\Request;
-
-use FireflyIII\Exceptions\FireflyException;
-use FireflyIII\Models\SpectreProvider;
-use FireflyIII\Services\Spectre\Object\Customer;
-
-/**
- * Class CreateLoginRequest
- */
-class CreateLoginRequest extends SpectreRequest
-{
- /** @var Customer */
- private $customer;
- /** @var array */
- private $mandatoryFields = [];
- /** @var SpectreProvider */
- private $provider;
-
- /**
- *
- * @throws FireflyException
- */
- public function call(): void
- {
- // add mandatory fields to login object
- $data = [
- 'customer_id' => $this->customer->getId(),
- 'country_code' => $this->provider->country_code,
- 'provider_code' => $this->provider->code,
- 'credentials' => $this->buildCredentials(),
- 'daily_refresh' => true,
- 'fetch_type' => 'recent',
- 'include_fake_providers' => true,
- ];
- $uri = '/api/v3/logins';
- $response = $this->sendSignedSpectrePost($uri, $data);
- echo '';
- print_r($response);
- exit;
- }
-
- /**
- * @param Customer $customer
- */
- public function setCustomer(Customer $customer): void
- {
- $this->customer = $customer;
- }
-
- /**
- * @param array $mandatoryFields
- */
- public function setMandatoryFields(array $mandatoryFields): void
- {
- $this->mandatoryFields = $mandatoryFields;
- }
-
- /**
- * @param SpectreProvider $provider
- */
- public function setProvider(SpectreProvider $provider): void
- {
- $this->provider = $provider;
- }
-
- /**
- * @return array
- * @throws FireflyException
- */
- private function buildCredentials(): array
- {
- $return = [];
- /** @var array $requiredField */
- foreach ($this->provider->data['required_fields'] as $requiredField) {
- $fieldName = $requiredField['name'];
- if (!isset($this->mandatoryFields[$fieldName])) {
- throw new FireflyException(sprintf('Mandatory field "%s" is missing from job.', $fieldName));
- }
- $return[$fieldName] = $this->mandatoryFields[$fieldName];
- }
-
- return $return;
- }
-
-
-}
\ No newline at end of file
diff --git a/app/Services/Spectre/Request/CreateTokenRequest.php b/app/Services/Spectre/Request/CreateTokenRequest.php
new file mode 100644
index 0000000000..adacab4ae8
--- /dev/null
+++ b/app/Services/Spectre/Request/CreateTokenRequest.php
@@ -0,0 +1,94 @@
+.
+ */
+
+declare(strict_types=1);
+
+namespace FireflyIII\Services\Spectre\Request;
+
+use FireflyIII\Services\Spectre\Object\Customer;
+use FireflyIII\Services\Spectre\Object\Token;
+
+
+/**
+ * Class CreateTokenRequest
+ */
+class CreateTokenRequest extends SpectreRequest
+{
+ /** @var Customer */
+ private $customer;
+
+ /** @var Token */
+ private $token;
+
+ /** @var string */
+ private $uri;
+
+ /**
+ *
+ * @throws \FireflyIII\Exceptions\FireflyException
+ */
+ public function call(): void
+ {
+ // add mandatory fields to login object
+ $data = [
+ 'data' => [
+ 'customer_id' => $this->customer->getId(),
+ 'fetch_type' => 'recent',
+ 'daily_refresh' => true,
+ 'include_fake_providers' => true,
+ 'show_consent_confirmation' => true,
+ 'credentials_strategy' => 'ask',
+ 'return_to' => $this->uri,
+ ],
+ ];
+ $uri = '/api/v3/tokens/create';
+ $response = $this->sendSignedSpectrePost($uri, $data);
+ $this->token = new Token($response['data']);
+
+ return;
+ }
+
+ /**
+ * @return Token
+ */
+ public function getToken(): Token
+ {
+ return $this->token;
+ }
+
+ /**
+ * @param Customer $customer
+ */
+ public function setCustomer(Customer $customer): void
+ {
+ $this->customer = $customer;
+ }
+
+ /**
+ * @param string $uri
+ */
+ public function setUri(string $uri): void
+ {
+ $this->uri = $uri;
+ }
+
+
+}
\ No newline at end of file
diff --git a/app/Services/Spectre/Request/ListLoginsRequest.php b/app/Services/Spectre/Request/ListLoginsRequest.php
deleted file mode 100644
index d69af2a026..0000000000
--- a/app/Services/Spectre/Request/ListLoginsRequest.php
+++ /dev/null
@@ -1,101 +0,0 @@
-.
- */
-declare(strict_types=1);
-
-namespace FireflyIII\Services\Spectre\Request;
-
-use FireflyIII\Services\Spectre\Object\Customer;
-use Log;
-
-/**
- * Class ListLoginsRequest
- */
-class ListLoginsRequest extends SpectreRequest
-{
- /** @var Customer */
- protected $customer;
-
- /** @var array */
- protected $logins = [];
-
- /**
- *
- * @throws \FireflyIII\Exceptions\FireflyException
- */
- public function call(): void
- {
- $hasNextPage = true;
- $nextId = 0;
- while ($hasNextPage) {
- Log::debug(sprintf('Now calling list-logins for next_id %d', $nextId));
- $parameters = ['customer_id' => $this->customer->getId(), 'from_id' => $nextId];
- $uri = '/api/v3/logins?' . http_build_query($parameters);
- $response = $this->sendSignedSpectreGet($uri, []);
-
- // count entries:
- Log::debug(sprintf('Found %d entries in data-array', count($response['data'])));
-
- // extract next ID
- $hasNextPage = false;
- if (isset($response['meta']['next_id']) && intval($response['meta']['next_id']) > $nextId) {
- $hasNextPage = true;
- $nextId = $response['meta']['next_id'];
- Log::debug(sprintf('Next ID is now %d.', $nextId));
- } else {
- Log::debug('No next page.');
- }
-
- // store providers:
- foreach ($response['data'] as $loginArray) {
- var_dump($loginArray);
- exit;
- }
- }
-
- return;
- }
-
- /**
- * @return Customer
- */
- public function getCustomer(): Customer
- {
- return $this->customer;
- }
-
- /**
- * @param Customer $customer
- */
- public function setCustomer(Customer $customer): void
- {
- $this->customer = $customer;
- }
-
- /**
- * @return array
- */
- public function getLogins(): array
- {
- return $this->logins;
- }
-
-
-}
diff --git a/app/Services/Spectre/Request/ListProvidersRequest.php b/app/Services/Spectre/Request/ListProvidersRequest.php
deleted file mode 100644
index 966023904c..0000000000
--- a/app/Services/Spectre/Request/ListProvidersRequest.php
+++ /dev/null
@@ -1,81 +0,0 @@
-.
- */
-declare(strict_types=1);
-
-namespace FireflyIII\Services\Spectre\Request;
-
-use Log;
-
-/**
- * Class ListProvidersRequest
- */
-class ListProvidersRequest extends SpectreRequest
-{
- /**
- * @var array
- */
- protected $providers = [];
-
- /**
- * @throws \Exception
- */
- public function call(): void
- {
- $hasNextPage = true;
- $nextId = 0;
- while ($hasNextPage) {
- Log::debug(sprintf('Now calling for next_id %d', $nextId));
- $parameters = ['include_fake_providers' => 'true', 'include_provider_fields' => 'true', 'from_id' => $nextId];
- $uri = '/api/v3/providers?' . http_build_query($parameters);
- $response = $this->sendSignedSpectreGet($uri, []);
-
- // count entries:
- Log::debug(sprintf('Found %d entries in data-array', count($response['data'])));
-
- // extract next ID
- $hasNextPage = false;
- if (isset($response['meta']['next_id']) && intval($response['meta']['next_id']) > $nextId) {
- $hasNextPage = true;
- $nextId = $response['meta']['next_id'];
- Log::debug(sprintf('Next ID is now %d.', $nextId));
- } else {
- Log::debug('No next page.');
- }
-
- // store providers:
- foreach ($response['data'] as $providerArray) {
- $providerId = $providerArray['id'];
- $this->providers[$providerId] = $providerArray;
- Log::debug(sprintf('Stored provider #%d', $providerId));
- }
- }
-
- return;
- }
-
- /**
- * @return array
- */
- public function getProviders(): array
- {
- return $this->providers;
- }
-}
diff --git a/app/Services/Spectre/Request/NewCustomerRequest.php b/app/Services/Spectre/Request/NewCustomerRequest.php
index ff36919d70..0c6960be90 100644
--- a/app/Services/Spectre/Request/NewCustomerRequest.php
+++ b/app/Services/Spectre/Request/NewCustomerRequest.php
@@ -37,19 +37,13 @@ class NewCustomerRequest extends SpectreRequest
*/
public function call(): void
{
- $data = [
+ $data = [
'data' => [
'identifier' => 'default_ff3_customer',
],
];
- $uri = '/api/v3/customers/';
- //$response = $this->sendSignedSpectrePost($uri, $data);
- $response = ['data' => [
- 'id' => 527858,
- 'identifier' => 'default_ff3_customer',
- 'secret' => 'qpZjRPJRTb6mMcQgwDkssZ3fQVVDPIH04zBlkKC6MvI',
- ],
- ];
+ $uri = '/api/v3/customers/';
+ $response = $this->sendSignedSpectrePost($uri, $data);
// create customer:
$this->customer = new Customer($response['data']);
diff --git a/app/Support/Import/Configuration/Spectre/InputMandatory.php b/app/Support/Import/Configuration/Spectre/InputMandatory.php
deleted file mode 100644
index c6182ff53a..0000000000
--- a/app/Support/Import/Configuration/Spectre/InputMandatory.php
+++ /dev/null
@@ -1,121 +0,0 @@
-.
- */
-declare(strict_types=1);
-
-namespace FireflyIII\Support\Import\Configuration\Spectre;
-
-use Crypt;
-use FireflyIII\Exceptions\FireflyException;
-use FireflyIII\Models\ImportJob;
-use FireflyIII\Models\SpectreProvider;
-use FireflyIII\Support\Import\Configuration\ConfigurationInterface;
-
-/**
- * Class InputMandatory
- */
-class InputMandatory implements ConfigurationInterface
-{
- /** @var ImportJob */
- private $job;
-
- /**
- * Get the data necessary to show the configuration screen.
- *
- * @return array
- *
- * @throws FireflyException
- */
- public function getData(): array
- {
- $config = $this->job->configuration;
- $providerId = $config['provider'];
- $provider = SpectreProvider::where('spectre_id', $providerId)->first();
- if (is_null($provider)) {
- throw new FireflyException(sprintf('Cannot find Spectre provider with ID #%d', $providerId));
- }
- $fields = $provider->data['required_fields'] ?? [];
- $positions = [];
- // Obtain a list of columns
- foreach ($fields as $key => $row) {
- $positions[$key] = $row['position'];
- }
- array_multisort($positions, SORT_ASC, $fields);
- $country = SelectCountry::$allCountries[$config['country']] ?? $config['country'];
-
- return compact('provider', 'country', 'fields');
- }
-
- /**
- * Return possible warning to user.
- *
- * @return string
- */
- public function getWarningMessage(): string
- {
- return '';
- }
-
- /**
- * @param ImportJob $job
- */
- public function setJob(ImportJob $job)
- {
- $this->job = $job;
- }
-
- /**
- * Store the result.
- *
- * @param array $data
- *
- * @return bool
- *
- * @throws FireflyException
- */
- public function storeConfiguration(array $data): bool
- {
- $config = $this->job->configuration;
- $providerId = $config['provider'];
- $provider = SpectreProvider::where('spectre_id', $providerId)->first();
- if (is_null($provider)) {
- throw new FireflyException(sprintf('Cannot find Spectre provider with ID #%d', $providerId));
- }
- $mandatory = [];
- $fields = $provider->data['required_fields'] ?? [];
- foreach ($fields as $field) {
- $name = $field['name'];
- $mandatory[$name] = Crypt::encrypt($data[$name]) ?? null;
- }
-
- // store in config of job:
- $config['mandatory-fields'] = $mandatory;
- $config['has-input-mandatory'] = true;
- $this->job->configuration = $config;
- $this->job->save();
-
- // try to grab login for this job. See what happens?
- // fire job that creates login object. user is redirected to "wait here" page (status page). Page should
- // refresh and go back to interactive when user is supposed to enter SMS code or something.
- // otherwise start downloading stuff
-
- return true;
- }
-}
diff --git a/app/Support/Import/Configuration/Spectre/SelectCountry.php b/app/Support/Import/Configuration/Spectre/SelectCountry.php
deleted file mode 100644
index eec76e8e54..0000000000
--- a/app/Support/Import/Configuration/Spectre/SelectCountry.php
+++ /dev/null
@@ -1,348 +0,0 @@
-.
- */
-declare(strict_types=1);
-
-namespace FireflyIII\Support\Import\Configuration\Spectre;
-
-use FireflyIII\Models\ImportJob;
-use FireflyIII\Models\SpectreProvider;
-use FireflyIII\Support\Import\Configuration\ConfigurationInterface;
-
-/**
- * Class SelectCountry
- */
-class SelectCountry implements ConfigurationInterface
-{
- /**
- * @var array
- */
- public static $allCountries
- = [
- 'AF' => 'Afghanistan',
- 'AX' => 'Aland Islands',
- 'AL' => 'Albania',
- 'DZ' => 'Algeria',
- 'AS' => 'American Samoa',
- 'AD' => 'Andorra',
- 'AO' => 'Angola',
- 'AI' => 'Anguilla',
- 'AQ' => 'Antarctica',
- 'AG' => 'Antigua and Barbuda',
- 'AR' => 'Argentina',
- 'AM' => 'Armenia',
- 'AW' => 'Aruba',
- 'AU' => 'Australia',
- 'AT' => 'Austria',
- 'AZ' => 'Azerbaijan',
- 'BS' => 'Bahamas',
- 'BH' => 'Bahrain',
- 'BD' => 'Bangladesh',
- 'BB' => 'Barbados',
- 'BY' => 'Belarus',
- 'BE' => 'Belgium',
- 'BZ' => 'Belize',
- 'BJ' => 'Benin',
- 'BM' => 'Bermuda',
- 'BT' => 'Bhutan',
- 'BO' => 'Bolivia',
- 'BQ' => 'Bonaire, Saint Eustatius and Saba',
- 'BA' => 'Bosnia and Herzegovina',
- 'BW' => 'Botswana',
- 'BV' => 'Bouvet Island',
- 'BR' => 'Brazil',
- 'IO' => 'British Indian Ocean Territory',
- 'VG' => 'British Virgin Islands',
- 'BN' => 'Brunei',
- 'BG' => 'Bulgaria',
- 'BF' => 'Burkina Faso',
- 'BI' => 'Burundi',
- 'KH' => 'Cambodia',
- 'CM' => 'Cameroon',
- 'CA' => 'Canada',
- 'CV' => 'Cape Verde',
- 'KY' => 'Cayman Islands',
- 'CF' => 'Central African Republic',
- 'TD' => 'Chad',
- 'CL' => 'Chile',
- 'CN' => 'China',
- 'CX' => 'Christmas Island',
- 'CC' => 'Cocos Islands',
- 'CO' => 'Colombia',
- 'KM' => 'Comoros',
- 'CK' => 'Cook Islands',
- 'CR' => 'Costa Rica',
- 'HR' => 'Croatia',
- 'CU' => 'Cuba',
- 'CW' => 'Curacao',
- 'CY' => 'Cyprus',
- 'CZ' => 'Czech Republic',
- 'CD' => 'Democratic Republic of the Congo',
- 'DK' => 'Denmark',
- 'DJ' => 'Djibouti',
- 'DM' => 'Dominica',
- 'DO' => 'Dominican Republic',
- 'TL' => 'East Timor',
- 'EC' => 'Ecuador',
- 'EG' => 'Egypt',
- 'SV' => 'El Salvador',
- 'GQ' => 'Equatorial Guinea',
- 'ER' => 'Eritrea',
- 'EE' => 'Estonia',
- 'ET' => 'Ethiopia',
- 'FK' => 'Falkland Islands',
- 'FO' => 'Faroe Islands',
- 'FJ' => 'Fiji',
- 'FI' => 'Finland',
- 'FR' => 'France',
- 'GF' => 'French Guiana',
- 'PF' => 'French Polynesia',
- 'TF' => 'French Southern Territories',
- 'GA' => 'Gabon',
- 'GM' => 'Gambia',
- 'GE' => 'Georgia',
- 'DE' => 'Germany',
- 'GH' => 'Ghana',
- 'GI' => 'Gibraltar',
- 'GR' => 'Greece',
- 'GL' => 'Greenland',
- 'GD' => 'Grenada',
- 'GP' => 'Guadeloupe',
- 'GU' => 'Guam',
- 'GT' => 'Guatemala',
- 'GG' => 'Guernsey',
- 'GN' => 'Guinea',
- 'GW' => 'Guinea-Bissau',
- 'GY' => 'Guyana',
- 'HT' => 'Haiti',
- 'HM' => 'Heard Island and McDonald Islands',
- 'HN' => 'Honduras',
- 'HK' => 'Hong Kong',
- 'HU' => 'Hungary',
- 'IS' => 'Iceland',
- 'IN' => 'India',
- 'ID' => 'Indonesia',
- 'IR' => 'Iran',
- 'IQ' => 'Iraq',
- 'IE' => 'Ireland',
- 'IM' => 'Isle of Man',
- 'IL' => 'Israel',
- 'IT' => 'Italy',
- 'CI' => 'Ivory Coast',
- 'JM' => 'Jamaica',
- 'JP' => 'Japan',
- 'JE' => 'Jersey',
- 'JO' => 'Jordan',
- 'KZ' => 'Kazakhstan',
- 'KE' => 'Kenya',
- 'KI' => 'Kiribati',
- 'XK' => 'Kosovo',
- 'KW' => 'Kuwait',
- 'KG' => 'Kyrgyzstan',
- 'LA' => 'Laos',
- 'LV' => 'Latvia',
- 'LB' => 'Lebanon',
- 'LS' => 'Lesotho',
- 'LR' => 'Liberia',
- 'LY' => 'Libya',
- 'LI' => 'Liechtenstein',
- 'LT' => 'Lithuania',
- 'LU' => 'Luxembourg',
- 'MO' => 'Macao',
- 'MK' => 'Macedonia',
- 'MG' => 'Madagascar',
- 'MW' => 'Malawi',
- 'MY' => 'Malaysia',
- 'MV' => 'Maldives',
- 'ML' => 'Mali',
- 'MT' => 'Malta',
- 'MH' => 'Marshall Islands',
- 'MQ' => 'Martinique',
- 'MR' => 'Mauritania',
- 'MU' => 'Mauritius',
- 'YT' => 'Mayotte',
- 'MX' => 'Mexico',
- 'FM' => 'Micronesia',
- 'MD' => 'Moldova',
- 'MC' => 'Monaco',
- 'MN' => 'Mongolia',
- 'ME' => 'Montenegro',
- 'MS' => 'Montserrat',
- 'MA' => 'Morocco',
- 'MZ' => 'Mozambique',
- 'MM' => 'Myanmar',
- 'NA' => 'Namibia',
- 'NR' => 'Nauru',
- 'NP' => 'Nepal',
- 'NL' => 'Netherlands',
- 'NC' => 'New Caledonia',
- 'NZ' => 'New Zealand',
- 'NI' => 'Nicaragua',
- 'NE' => 'Niger',
- 'NG' => 'Nigeria',
- 'NU' => 'Niue',
- 'NF' => 'Norfolk Island',
- 'KP' => 'North Korea',
- 'MP' => 'Northern Mariana Islands',
- 'NO' => 'Norway',
- 'OM' => 'Oman',
- 'PK' => 'Pakistan',
- 'PW' => 'Palau',
- 'PS' => 'Palestinian Territory',
- 'PA' => 'Panama',
- 'PG' => 'Papua New Guinea',
- 'PY' => 'Paraguay',
- 'PE' => 'Peru',
- 'PH' => 'Philippines',
- 'PN' => 'Pitcairn',
- 'PL' => 'Poland',
- 'PT' => 'Portugal',
- 'PR' => 'Puerto Rico',
- 'QA' => 'Qatar',
- 'CG' => 'Republic of the Congo',
- 'RE' => 'Reunion',
- 'RO' => 'Romania',
- 'RU' => 'Russia',
- 'RW' => 'Rwanda',
- 'BL' => 'Saint Barthelemy',
- 'SH' => 'Saint Helena',
- 'KN' => 'Saint Kitts and Nevis',
- 'LC' => 'Saint Lucia',
- 'MF' => 'Saint Martin',
- 'PM' => 'Saint Pierre and Miquelon',
- 'VC' => 'Saint Vincent and the Grenadines',
- 'WS' => 'Samoa',
- 'SM' => 'San Marino',
- 'ST' => 'Sao Tome and Principe',
- 'SA' => 'Saudi Arabia',
- 'SN' => 'Senegal',
- 'RS' => 'Serbia',
- 'SC' => 'Seychelles',
- 'SL' => 'Sierra Leone',
- 'SG' => 'Singapore',
- 'SX' => 'Sint Maarten',
- 'SK' => 'Slovakia',
- 'SI' => 'Slovenia',
- 'SB' => 'Solomon Islands',
- 'SO' => 'Somalia',
- 'ZA' => 'South Africa',
- 'GS' => 'South Georgia and the South Sandwich Islands',
- 'KR' => 'South Korea',
- 'SS' => 'South Sudan',
- 'ES' => 'Spain',
- 'LK' => 'Sri Lanka',
- 'SD' => 'Sudan',
- 'SR' => 'Suriname',
- 'SJ' => 'Svalbard and Jan Mayen',
- 'SZ' => 'Swaziland',
- 'SE' => 'Sweden',
- 'CH' => 'Switzerland',
- 'SY' => 'Syria',
- 'TW' => 'Taiwan',
- 'TJ' => 'Tajikistan',
- 'TZ' => 'Tanzania',
- 'TH' => 'Thailand',
- 'TG' => 'Togo',
- 'TK' => 'Tokelau',
- 'TO' => 'Tonga',
- 'TT' => 'Trinidad and Tobago',
- 'TN' => 'Tunisia',
- 'TR' => 'Turkey',
- 'TM' => 'Turkmenistan',
- 'TC' => 'Turks and Caicos Islands',
- 'TV' => 'Tuvalu',
- 'VI' => 'U.S. Virgin Islands',
- 'UG' => 'Uganda',
- 'UA' => 'Ukraine',
- 'AE' => 'United Arab Emirates',
- 'GB' => 'United Kingdom',
- 'US' => 'United States',
- 'UM' => 'United States Minor Outlying Islands',
- 'UY' => 'Uruguay',
- 'UZ' => 'Uzbekistan',
- 'VU' => 'Vanuatu',
- 'VA' => 'Vatican',
- 'VE' => 'Venezuela',
- 'VN' => 'Vietnam',
- 'WF' => 'Wallis and Futuna',
- 'EH' => 'Western Sahara',
- 'YE' => 'Yemen',
- 'ZM' => 'Zambia',
- 'ZW' => 'Zimbabwe',
- 'XF' => 'Fake Country (for testing)',
- 'XO' => 'Other financial applications',
- ];
- /** @var ImportJob */
- private $job;
-
- /**
- * Get the data necessary to show the configuration screen.
- *
- * @return array
- */
- public function getData(): array
- {
- $providers = SpectreProvider::get();
- $countries = [];
- /** @var SpectreProvider $provider */
- foreach ($providers as $provider) {
- $countries[$provider->country_code] = self::$allCountries[$provider->country_code] ?? $provider->country_code;
- }
- asort($countries);
-
- return compact('countries');
- }
-
- /**
- * Return possible warning to user.
- *
- * @return string
- */
- public function getWarningMessage(): string
- {
- return '';
- }
-
- /**
- * @param ImportJob $job
- */
- public function setJob(ImportJob $job)
- {
- $this->job = $job;
- }
-
- /**
- * Store the result.
- *
- * @param array $data
- *
- * @return bool
- */
- public function storeConfiguration(array $data): bool
- {
- $config = $this->job->configuration;
- $config['country'] = $data['country_code'] ?? 'XF'; // default to fake country.
- $config['selected-country'] = true;
- $this->job->configuration = $config;
- $this->job->save();
-
- return true;
- }
-}
diff --git a/app/Support/Import/Configuration/Spectre/SelectProvider.php b/app/Support/Import/Configuration/Spectre/SelectProvider.php
deleted file mode 100644
index 843a722c04..0000000000
--- a/app/Support/Import/Configuration/Spectre/SelectProvider.php
+++ /dev/null
@@ -1,93 +0,0 @@
-.
- */
-declare(strict_types=1);
-
-namespace FireflyIII\Support\Import\Configuration\Spectre;
-
-use FireflyIII\Models\ImportJob;
-use FireflyIII\Models\SpectreProvider;
-use FireflyIII\Support\Import\Configuration\ConfigurationInterface;
-
-/**
- * Class SelectProvider
- */
-class SelectProvider implements ConfigurationInterface
-{
- /** @var ImportJob */
- private $job;
-
- /**
- * Get the data necessary to show the configuration screen.
- *
- * @return array
- */
- public function getData(): array
- {
- $config = $this->job->configuration;
- $selection = SpectreProvider::where('country_code', $config['country'])->where('status', 'active')->get();
- $providers = [];
- /** @var SpectreProvider $provider */
- foreach ($selection as $provider) {
- $providerId = $provider->spectre_id;
- $name = $provider->data['name'];
- $providers[$providerId] = $name;
- }
- $country = SelectCountry::$allCountries[$config['country']] ?? $config['country'];
-
- return compact('providers', 'country');
- }
-
- /**
- * Return possible warning to user.
- *
- * @return string
- */
- public function getWarningMessage(): string
- {
- return '';
- }
-
- /**
- * @param ImportJob $job
- */
- public function setJob(ImportJob $job)
- {
- $this->job = $job;
- }
-
- /**
- * Store the result.
- *
- * @param array $data
- *
- * @return bool
- */
- public function storeConfiguration(array $data): bool
- {
- $config = $this->job->configuration;
- $config['provider'] = intval($data['provider_code']) ?? 0; // default to fake country.
- $config['selected-provider'] = true;
- $this->job->configuration = $config;
- $this->job->save();
-
- return true;
- }
-}
diff --git a/app/Support/Import/Information/SpectreInformation.php b/app/Support/Import/Information/SpectreInformation.php
deleted file mode 100644
index 0cc93f8992..0000000000
--- a/app/Support/Import/Information/SpectreInformation.php
+++ /dev/null
@@ -1,217 +0,0 @@
-.
- */
-declare(strict_types=1);
-
-namespace FireflyIII\Support\Import\Information;
-
-use FireflyIII\Exceptions\FireflyException;
-use FireflyIII\Services\Bunq\Object\Alias;
-use FireflyIII\Services\Bunq\Object\MonetaryAccountBank;
-use FireflyIII\Services\Bunq\Request\DeleteDeviceSessionRequest;
-use FireflyIII\Services\Bunq\Request\DeviceSessionRequest;
-use FireflyIII\Services\Bunq\Request\ListMonetaryAccountRequest;
-use FireflyIII\Services\Bunq\Request\ListUserRequest;
-use FireflyIII\Services\Bunq\Token\SessionToken;
-use FireflyIII\Support\CacheProperties;
-use FireflyIII\User;
-use Illuminate\Support\Collection;
-use Log;
-use Preferences;
-
-/**
- * Class SpectreInformation
- */
-class SpectreInformation implements InformationInterface
-{
- /** @var User */
- private $user;
-
- /**
- * Returns a collection of accounts. Preferrably, these follow a uniform Firefly III format so they can be managed over banks.
- *
- * The format for these bank accounts is basically this:
- *
- * id: bank specific id
- * name: bank appointed name
- * number: account number (usually IBAN)
- * currency: ISO code of currency
- * balance: current balance
- *
- *
- * any other fields are optional but can be useful:
- * image: logo or account specific thing
- * color: any associated color.
- *
- * @return array
- *
- * @throws FireflyException
- * @throws \Exception
- */
- public function getAccounts(): array
- {
- // cache for an hour:
- $cache = new CacheProperties;
- $cache->addProperty('bunq.get-accounts');
- $cache->addProperty(date('dmy h'));
- if ($cache->has()) {
- return $cache->get(); // @codeCoverageIgnore
- }
- Log::debug('Now in getAccounts()');
- $sessionToken = $this->startSession();
- $userId = $this->getUserInformation($sessionToken);
- // get list of Bunq accounts:
- $accounts = $this->getMonetaryAccounts($sessionToken, $userId);
- $return = [];
- /** @var MonetaryAccountBank $account */
- foreach ($accounts as $account) {
- $current = [
- 'id' => $account->getId(),
- 'name' => $account->getDescription(),
- 'currency' => $account->getCurrency(),
- 'balance' => $account->getBalance()->getValue(),
- 'color' => $account->getSetting()->getColor(),
- ];
- /** @var Alias $alias */
- foreach ($account->getAliases() as $alias) {
- if ('IBAN' === $alias->getType()) {
- $current['number'] = $alias->getValue();
- }
- }
- $return[] = $current;
- }
- $cache->store($return);
-
- $this->closeSession($sessionToken);
-
- return $return;
- }
-
- /**
- * Set the user for this Prerequisites-routine. Class is expected to implement and save this.
- *
- * @param User $user
- */
- public function setUser(User $user): void
- {
- $this->user = $user;
- }
-
- /**
- * @param SessionToken $sessionToken
- *
- * @throws \Exception
- */
- private function closeSession(SessionToken $sessionToken): void
- {
- Log::debug('Going to close session');
- $apiKey = Preferences::getForUser($this->user, 'bunq_api_key')->data;
- $serverPublicKey = Preferences::getForUser($this->user, 'bunq_server_public_key')->data;
- $privateKey = Preferences::getForUser($this->user, 'bunq_private_key')->data;
- $request = new DeleteDeviceSessionRequest();
- $request->setSecret($apiKey);
- $request->setPrivateKey($privateKey);
- $request->setServerPublicKey($serverPublicKey);
- $request->setSessionToken($sessionToken);
- $request->call();
-
- return;
- }
-
- /**
- * @param SessionToken $sessionToken
- * @param int $userId
- *
- * @return Collection
- *
- * @throws \Exception
- */
- private function getMonetaryAccounts(SessionToken $sessionToken, int $userId): Collection
- {
- $apiKey = Preferences::getForUser($this->user, 'bunq_api_key')->data;
- $serverPublicKey = Preferences::getForUser($this->user, 'bunq_server_public_key')->data;
- $privateKey = Preferences::getForUser($this->user, 'bunq_private_key')->data;
- $request = new ListMonetaryAccountRequest;
-
- $request->setSessionToken($sessionToken);
- $request->setSecret($apiKey);
- $request->setServerPublicKey($serverPublicKey);
- $request->setPrivateKey($privateKey);
- $request->setUserId($userId);
- $request->call();
-
- return $request->getMonetaryAccounts();
- }
-
- /**
- * @param SessionToken $sessionToken
- *
- * @return int
- *
- * @throws FireflyException
- * @throws \Exception
- */
- private function getUserInformation(SessionToken $sessionToken): int
- {
- $apiKey = Preferences::getForUser($this->user, 'bunq_api_key')->data;
- $serverPublicKey = Preferences::getForUser($this->user, 'bunq_server_public_key')->data;
- $privateKey = Preferences::getForUser($this->user, 'bunq_private_key')->data;
- $request = new ListUserRequest;
- $request->setSessionToken($sessionToken);
- $request->setSecret($apiKey);
- $request->setServerPublicKey($serverPublicKey);
- $request->setPrivateKey($privateKey);
- $request->call();
- // return the first that isn't null?
- $company = $request->getUserCompany();
- if ($company->getId() > 0) {
- return $company->getId();
- }
- $user = $request->getUserPerson();
- if ($user->getId() > 0) {
- return $user->getId();
- }
- throw new FireflyException('Expected user or company from Bunq, but got neither.');
- }
-
- /**
- * @return SessionToken
- *
- * @throws \Exception
- */
- private function startSession(): SessionToken
- {
- Log::debug('Now in startSession.');
- $apiKey = Preferences::getForUser($this->user, 'bunq_api_key')->data;
- $serverPublicKey = Preferences::getForUser($this->user, 'bunq_server_public_key')->data;
- $privateKey = Preferences::getForUser($this->user, 'bunq_private_key')->data;
- $installationToken = Preferences::getForUser($this->user, 'bunq_installation_token')->data;
- $request = new DeviceSessionRequest();
- $request->setSecret($apiKey);
- $request->setServerPublicKey($serverPublicKey);
- $request->setPrivateKey($privateKey);
- $request->setInstallationToken($installationToken);
- $request->call();
- $sessionToken = $request->getSessionToken();
- Log::debug(sprintf('Now have got session token: %s', serialize($sessionToken)));
-
- return $sessionToken;
- }
-}
diff --git a/public/js/ff/import/status.js b/public/js/ff/import/status.js
index 0f65e71b60..9fa0198516 100644
--- a/public/js/ff/import/status.js
+++ b/public/js/ff/import/status.js
@@ -34,10 +34,12 @@ var knownErrors = 0;
$(function () {
"use strict";
+ console.log('in start');
timeOutId = setTimeout(checkJobStatus, startInterval);
$('.start-job').click(startJob);
if (job.configuration['auto-start']) {
+ console.log('Called startJob()!');
startJob();
}
});
@@ -46,6 +48,7 @@ $(function () {
* Downloads some JSON and responds to its content to see what the status is of the current import.
*/
function checkJobStatus() {
+ console.log('in checkJobStatus');
$.getJSON(jobStatusUri).done(reportOnJobStatus).fail(reportFailedJob);
}
@@ -53,6 +56,7 @@ function checkJobStatus() {
* This method is called when the JSON query returns an error. If possible, this error is relayed to the user.
*/
function reportFailedJob(jqxhr, textStatus, error) {
+ console.log('in reportFailedJob');
// hide all possible boxes:
$('.statusbox').hide();
@@ -72,6 +76,7 @@ function reportFailedJob(jqxhr, textStatus, error) {
* @param data
*/
function reportOnJobStatus(data) {
+ console.log('in reportOnJobStatus: ' + data.status);
switch (data.status) {
case "configured":
@@ -80,6 +85,10 @@ function reportOnJobStatus(data) {
$('.statusbox').hide();
$('.status_configured').show();
}
+ if (job.configuration['auto-start']) {
+ console.log('Job is auto start. Check status again in 500ms.');
+ timeOutId = setTimeout(checkJobStatus, interval);
+ }
break;
case "running":
// job is running! Show the running box:
@@ -122,7 +131,13 @@ function reportOnJobStatus(data) {
// show the fatal error box:
$('.fatal_error').show();
break;
+ case "configuring":
+ // redirect back to configure screen.
+ console.log('Will now redirect to ' + jobConfigureUri);
+ window.location = jobConfigureUri;
+ break;
default:
+ console.error('Cannot handle job status ' + data.status);
break;
}
diff --git a/resources/views/import/spectre/redirect.twig b/resources/views/import/spectre/redirect.twig
new file mode 100644
index 0000000000..e76c8b4bdb
--- /dev/null
+++ b/resources/views/import/spectre/redirect.twig
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+ Page Redirection
+
+
+
+If you are not redirected automatically, follow this link to Spectre..
+
+#}
\ No newline at end of file
diff --git a/resources/views/import/status.twig b/resources/views/import/status.twig
index 2844f818fa..650c224fac 100644
--- a/resources/views/import/status.twig
+++ b/resources/views/import/status.twig
@@ -149,6 +149,7 @@
var langImportMultiError = '{{ trans('import.status_errors_multi')|escape('js') }}';
var jobStatusUri = '{{ route('import.status.json', [job.key]) }}';
var jobStartUri = '{{ route('import.start', [job.key]) }}';
+ var jobConfigureUri = '{{ route('import.configure', [job.key]) }}';
var token = '{{ csrf_token() }}';
var job = {{ job|json_encode|raw }};