From a7585e3040ce2292689df74d1cd7de8c6ac07397 Mon Sep 17 00:00:00 2001 From: James Cole Date: Fri, 21 Dec 2018 06:51:00 +0100 Subject: [PATCH] Fix for #1985 --- .../Bunq/ChooseAccountsHandler.php | 10 +- .../Import/Routine/Bunq/PaymentConverter.php | 244 ++++++++++++++++++ .../Routine/Bunq/StageImportDataHandler.php | 156 +---------- .../Import/Routine/Bunq/StageNewHandler.php | 24 +- 4 files changed, 285 insertions(+), 149 deletions(-) create mode 100644 app/Support/Import/Routine/Bunq/PaymentConverter.php diff --git a/app/Support/Import/JobConfiguration/Bunq/ChooseAccountsHandler.php b/app/Support/Import/JobConfiguration/Bunq/ChooseAccountsHandler.php index 8b1558202f..fe81936b11 100644 --- a/app/Support/Import/JobConfiguration/Bunq/ChooseAccountsHandler.php +++ b/app/Support/Import/JobConfiguration/Bunq/ChooseAccountsHandler.php @@ -93,6 +93,7 @@ class ChooseAccountsHandler implements BunqJobConfigurationInterface * This is used to properly map transfers. */ $ibanToAsset = []; + Log::debug('Going to map IBANs for easy mapping later on.'); if (0 === \count($accounts)) { throw new FireflyException('No bunq accounts found. Import cannot continue.'); // @codeCoverageIgnore } @@ -106,10 +107,17 @@ class ChooseAccountsHandler implements BunqJobConfigurationInterface $bunqId = (int)$bunqId; $localId = (int)$localId; + Log::debug(sprintf('Now trying to link bunq acount #%d with Firefly III account %d', $bunqId, $localId)); + // validate each $bunqId = $this->validBunqAccount($bunqId); $accountId = $this->validLocalAccount($localId); - $bunqIban = $this->getBunqIban($bunqId); + + Log::debug(sprintf('After validation: bunq account #%d with Firefly III account %d', $bunqId, $localId)); + + $bunqIban = $this->getBunqIban($bunqId); + + Log::debug(sprintf('IBAN for bunq account #%d is "%s"', $bunqId, $bunqIban)); if (null !== $bunqIban) { $ibanToAsset[$bunqIban] = $accountId; } diff --git a/app/Support/Import/Routine/Bunq/PaymentConverter.php b/app/Support/Import/Routine/Bunq/PaymentConverter.php new file mode 100644 index 0000000000..d5da24f9b8 --- /dev/null +++ b/app/Support/Import/Routine/Bunq/PaymentConverter.php @@ -0,0 +1,244 @@ +. + */ + +declare(strict_types=1); + +namespace FireflyIII\Support\Import\Routine\Bunq; + +use bunq\Model\Generated\Endpoint\Payment as BunqPayment; +use bunq\Model\Generated\Object\LabelMonetaryAccount; +use Carbon\Carbon; +use FireflyIII\Exceptions\FireflyException; +use FireflyIII\Factory\AccountFactory; +use FireflyIII\Models\Account as LocalAccount; +use FireflyIII\Models\AccountType; +use FireflyIII\Models\ImportJob; +use FireflyIII\Models\TransactionType; +use FireflyIII\Repositories\Account\AccountRepositoryInterface; +use FireflyIII\Repositories\ImportJob\ImportJobRepositoryInterface; +use Log; + +/** + * Class PaymentConverter + */ +class PaymentConverter +{ + /** @var AccountFactory */ + private $accountFactory; + /** @var AccountRepositoryInterface */ + private $accountRepos; + /** @var array */ + private $configuration; + /** @var ImportJob */ + private $importJob; + /** @var ImportJobRepositoryInterface */ + private $importJobRepos; + + public function __construct() + { + $this->accountRepos = app(AccountRepositoryInterface::class); + $this->importJobRepos = app(ImportJobRepositoryInterface::class); + $this->accountFactory = app(AccountFactory::class); + if ('testing' === config('app.env')) { + Log::warning(sprintf('%s should not be instantiated in the TEST environment!', \get_class($this))); + } + } + + /** + * Convert a bunq transaction to a usable transaction for Firefly III. + * + * @param BunqPayment $payment + * + * @return array + * @throws FireflyException + */ + public function convert(BunqPayment $payment, LocalAccount $source): array + { + $paymentId = $payment->getId(); + Log::debug(sprintf('Now in convert() for payment with ID #%d', $paymentId)); + Log::debug(sprintf('Source account is assumed to be "%s" (#%d)', $source->name, $source->id)); + $type = TransactionType::WITHDRAWAL; + $counterParty = $payment->getCounterpartyAlias(); + $amount = $payment->getAmount(); + + // some debug info: + Log::debug('Assume its a witdrawal'); + Log::debug(sprintf('Subtype is %s', $payment->getSubType())); + Log::debug(sprintf('Type is %s', $payment->getType())); + Log::debug(sprintf('Amount is %s %s', $amount->getCurrency(), $amount->getValue())); + + $expected = AccountType::EXPENSE; + if (1 === bccomp($amount->getValue(), '0')) { + // amount + means that its a deposit. + $expected = AccountType::REVENUE; + $type = TransactionType::DEPOSIT; + Log::debug(sprintf('Amount is %s %s, so assume this is a deposit.', $amount->getCurrency(), $amount->getValue())); + } + Log::debug(sprintf('Now going to convert counter party to Firefly III account. Expect it to be a "%s" account.', $expected)); + $destination = $this->convertToAccount($counterParty, $expected); + + // switch source and destination if necessary. + if (1 === bccomp($amount->getValue(), '0')) { + Log::debug('Because amount is > 0, will now swap source with destination.'); + [$source, $destination] = [$destination, $source]; + } + + if ($source->accountType->type === AccountType::ASSET && $destination->accountType->type === AccountType::ASSET) { + $type = TransactionType::TRANSFER; + Log::debug('Because both transctions are asset, will make it a transfer.'); + } + $created = new Carbon($payment->getCreated()); + + $description = $payment->getDescription(); + if ('' === $payment->getDescription() && 'SAVINGS' === $payment->getType()) { + $description = 'Auto-save for savings goal.'; + } + + $storeData = [ + 'user' => $this->importJob->user_id, + 'type' => $type, + 'date' => $created->format('Y-m-d'), + 'timestamp' => $created->toAtomString(), + 'description' => $description, + 'piggy_bank_id' => null, + 'piggy_bank_name' => null, + 'bill_id' => null, + 'bill_name' => null, + 'tags' => [$payment->getType(), $payment->getSubType()], + 'internal_reference' => $paymentId, + 'external_id' => $paymentId, + 'notes' => null, + 'bunq_payment_id' => $paymentId, + 'original-source' => sprintf('bunq-v%s', config('firefly.version')), + '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, + ], + ], + ]; + Log::info(sprintf('Parsed %s: "%s" (%s).', $created->format('Y-m-d'), $storeData['description'], $storeData['transactions'][0]['amount'])); + + return $storeData; + + } + + /** + * @param ImportJob $importJob + */ + public function setImportJob(ImportJob $importJob): void + { + $this->importJob = $importJob; + $this->accountRepos->setUser($importJob->user); + $this->importJobRepos->setUser($importJob->user); + $this->accountFactory->setUser($importJob->user); + $this->configuration = $this->importJobRepos->getConfiguration($importJob); + } + + /** + * @param LabelMonetaryAccount $party + * @param string $expectedType + * + * @return LocalAccount + * @throws FireflyException + */ + private function convertToAccount(LabelMonetaryAccount $party, string $expectedType): LocalAccount + { + Log::debug(sprintf('in convertToAccount() with LabelMonetaryAccount')); + if (null !== $party->getIban()) { + Log::debug(sprintf('Opposing party has IBAN "%s"', $party->getIban())); + + // find account in 'bunq-iban' array first. + $bunqIbans = $this->configuration['bunq-iban'] ?? []; + Log::debug('Bunq ibans configuration is', $bunqIbans); + + if (isset($bunqIbans[$party->getIban()])) { + Log::debug('IBAN is known in array.'); + $accountId = (int)$bunqIbans[$party->getIban()]; + $result = $this->accountRepos->findNull($accountId); + if (null !== $result) { + Log::debug(sprintf('Search for #%s (IBAN "%s"), found "%s" (#%d)', $accountId, $party->getIban(), $result->name, $result->id)); + + return $result; + } + } + + // find opposing party by IBAN second. + $result = $this->accountRepos->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->accountRepos->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; + } + } + } + Log::debug('Found no account for opposing party, must create a new one.'); + + // create new account: + $data = [ + 'user_id' => $this->importJob->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 NEW "%s" account "%s" (#%d)', + $party->getLabelUser()->getDisplayName(), + $expectedType, + $account->name, $account->id + ) + ); + + return $account; + } + + +} \ No newline at end of file diff --git a/app/Support/Import/Routine/Bunq/StageImportDataHandler.php b/app/Support/Import/Routine/Bunq/StageImportDataHandler.php index efcba5133c..cb62f8c186 100644 --- a/app/Support/Import/Routine/Bunq/StageImportDataHandler.php +++ b/app/Support/Import/Routine/Bunq/StageImportDataHandler.php @@ -24,15 +24,12 @@ declare(strict_types=1); namespace FireflyIII\Support\Import\Routine\Bunq; use bunq\Model\Generated\Endpoint\Payment as BunqPayment; -use bunq\Model\Generated\Object\LabelMonetaryAccount; -use Carbon\Carbon; use FireflyIII\Exceptions\FireflyException; use FireflyIII\Factory\AccountFactory; use FireflyIII\Models\Account as LocalAccount; use FireflyIII\Models\AccountType; use FireflyIII\Models\ImportJob; use FireflyIII\Models\Preference; -use FireflyIII\Models\TransactionType; use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Repositories\ImportJob\ImportJobRepositoryInterface; use FireflyIII\Services\Bunq\ApiContext; @@ -44,8 +41,10 @@ use Log; */ class StageImportDataHandler { + /** @var int */ private const DOWNLOAD_BACKWARDS = 1; - private const DOWNLOAD_FORWARDS = 2; + /** @var int */ + private const DOWNLOAD_FORWARDS = 2; /** @var bool */ public $stillRunning; @@ -53,6 +52,8 @@ class StageImportDataHandler private $accountFactory; /** @var AccountRepositoryInterface */ private $accountRepository; + /** @var PaymentConverter */ + private $converter; /** @var ImportJob */ private $importJob; /** @var array */ @@ -68,6 +69,7 @@ class StageImportDataHandler { $this->stillRunning = true; $this->timeStart = microtime(true); + $this->converter = app(PaymentConverter::class); } @@ -95,6 +97,7 @@ class StageImportDataHandler public function run(): void { $this->getContext(); + $this->converter->setImportJob($this->importJob); $config = $this->repository->getConfiguration($this->importJob); $accounts = $config['accounts'] ?? []; $mapping = $config['mapping'] ?? []; @@ -141,150 +144,9 @@ class StageImportDataHandler private function convertPayment(BunqPayment $payment, int $bunqAccountId, LocalAccount $source): array { Log::debug(sprintf('Now at payment with ID #%d', $payment->getId())); - Log::debug(sprintf('Object dump: %s', print_r($payment, true))); - $type = TransactionType::WITHDRAWAL; - $counterParty = $payment->getCounterpartyAlias(); - $amount = $payment->getAmount(); - $paymentId = $payment->getId(); - - // is there meta data to indicate this is a saving or something? - Log::debug(sprintf('Subtype is %s', $payment->getSubType())); - - - - - Log::debug(sprintf('Amount is %s %s', $amount->getCurrency(), $amount->getValue())); - $expected = AccountType::EXPENSE; - if (1 === bccomp($amount->getValue(), '0')) { - // amount + means that its a deposit. - $expected = AccountType::REVENUE; - $type = TransactionType::DEPOSIT; - Log::debug('Will make opposing account revenue.'); - } - $destination = $this->convertToAccount($counterParty, $expected); - - // switch source and destination if necessary. - if (1 === bccomp($amount->getValue(), '0')) { - Log::debug('Will make it a deposit.'); - [$source, $destination] = [$destination, $source]; - } - - if ($source->accountType->type === AccountType::ASSET && $destination->accountType->type === AccountType::ASSET) { - $type = TransactionType::TRANSFER; - Log::debug('Both are assets, will make transfer.'); - } - $created = new Carbon($payment->getCreated()); - $storeData = [ - 'user' => $this->importJob->user_id, - 'type' => $type, - 'date' => $created->format('Y-m-d'), - 'timestamp' => $created->toAtomString(), - 'description' => $payment->getDescription(), - 'piggy_bank_id' => null, - 'piggy_bank_name' => null, - 'bill_id' => null, - 'bill_name' => null, - 'tags' => [$payment->getType(), $payment->getSubType()], - 'internal_reference' => $paymentId, - 'external_id' => $paymentId, - 'notes' => null, - 'bunq_payment_id' => $paymentId, - 'original-source' => sprintf('bunq-v%s', config('firefly.version')), - '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, - ], - ], - ]; - Log::info(sprintf('Parsed %s: "%s" (%s).', $created->format('Y-m-d'), $storeData['description'], $storeData['transactions'][0]['amount'])); - - return $storeData; - } - - /** - * @param LabelMonetaryAccount $party - * @param string $expectedType - * - * @return LocalAccount - * @throws FireflyException - */ - private function convertToAccount(LabelMonetaryAccount $party, string $expectedType): LocalAccount - { - - Log::debug(sprintf('in convertToAccount() with LabelMonetaryAccount: %s', '')); - if (null !== $party->getIban()) { - // find account in 'bunq-iban' array first. - $bunqIbans = $this->jobConfiguration['bunq-iban'] ?? []; - if (isset($bunqIbans[$party->getIban()])) { - $accountId = (int)$bunqIbans[$party->getIban()]; - $result = $this->accountRepository->findNull($accountId); - if (null !== $result) { - Log::debug( - sprintf('Search for #%s (based on IBAN %s) resulted in account %s (#%d)', $accountId, $party->getIban(), $result->name, $result->id) - ); - - return $result; - } - } - - // find opposing party by IBAN second. - $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->importJob->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; + return $this->converter->convert($payment, $source); + exit; } /** diff --git a/app/Support/Import/Routine/Bunq/StageNewHandler.php b/app/Support/Import/Routine/Bunq/StageNewHandler.php index 20ef9712ca..30933bea73 100644 --- a/app/Support/Import/Routine/Bunq/StageNewHandler.php +++ b/app/Support/Import/Routine/Bunq/StageNewHandler.php @@ -94,6 +94,7 @@ class StageNewHandler */ private function listAccounts(): array { + Log::debug('Now in StageNewHandler::listAccounts()'); $accounts = []; /** @var MonetaryAccount $lister */ $lister = app(MonetaryAccount::class); @@ -112,18 +113,22 @@ class StageNewHandler $array = null; switch (\get_class($object)) { case MonetaryAccountBank::class: + Log::debug('Going to convert a MonetaryAccountBank'); /** @var MonetaryAccountBank $object */ $array = $this->processMab($object); break; case MonetaryAccountJoint::class: + Log::debug('Going to convert a MonetaryAccountJoint'); /** @var MonetaryAccountJoint $object */ $array = $this->processMaj($object); break; case MonetaryAccountLight::class: + Log::debug('Going to convert a MonetaryAccountLight'); /** @var MonetaryAccountLight $object */ $array = $this->processMal($object); break; case MonetaryAccountSavings::class; + Log::debug('Going to convert a MonetaryAccountSavings'); /** @var MonetaryAccountSavings $object */ $array = $this->processMas($object); break; @@ -134,12 +139,13 @@ class StageNewHandler // @codeCoverageIgnoreEnd } if (null !== $array) { + Log::debug('Array is not null'); $accounts[] = $array; $this->reportFinding($array); } } } - Log::info(sprintf('Found %d account(s) at bunq', \count($accounts))); + Log::info(sprintf('Found %d account(s) at bunq', \count($accounts)), $accounts); return $accounts; } @@ -226,6 +232,10 @@ class StageNewHandler 'name' => $alias->getName(), 'value' => $alias->getValue(), ]; + // store IBAN alias separately: + if ('IBAN' === $alias->getType()) { + $return['iban'] = $alias->getValue(); + } } } $coOwners = $maj->getAllCoOwner() ?? []; @@ -279,6 +289,10 @@ class StageNewHandler 'name' => $alias->getName(), 'value' => $alias->getValue(), ]; + // store IBAN alias separately: + if ('IBAN' === $alias->getType()) { + $return['iban'] = $alias->getValue(); + } } } @@ -292,6 +306,7 @@ class StageNewHandler */ private function processMas(MonetaryAccountSavings $object): array { + Log::debug('Now in processMas()'); $setting = $object->getSetting(); $return = [ 'id' => $object->getId(), @@ -312,13 +327,19 @@ class StageNewHandler ]; } if (null !== $object->getAlias()) { + Log::debug('MAS has aliases'); /** @var Pointer $alias */ foreach ($object->getAlias() as $alias) { + Log::debug(sprintf('Alias type is "%s", with name "%s" and value "%s"', $alias->getType(), $alias->getName(), $alias->getValue())); $return['aliases'][] = [ 'type' => $alias->getType(), 'name' => $alias->getName(), 'value' => $alias->getValue(), ]; + // store IBAN alias separately: + if ('IBAN' === $alias->getType()) { + $return['iban'] = $alias->getValue(); + } } } $goal = $object->getSavingsGoal(); @@ -327,6 +348,7 @@ class StageNewHandler 'value' => $goal->getValue(), 'percentage' => $object->getSavingsGoalProgress(), ]; + Log::debug('End of processMas()', $return); return $return; }