| 
									
										
										
										
											2023-07-25 09:01:44 +02:00
										 |  |  | <?php | 
					
						
							| 
									
										
										
										
											2023-07-28 16:14:55 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-07-25 09:01:44 +02:00
										 |  |  | /* | 
					
						
							|  |  |  |  * ExchangeRateConverter.php | 
					
						
							|  |  |  |  * Copyright (c) 2023 james@firefly-iii.org | 
					
						
							|  |  |  |  * | 
					
						
							|  |  |  |  * This file is part of Firefly III (https://github.com/firefly-iii). | 
					
						
							|  |  |  |  * | 
					
						
							|  |  |  |  * This program is free software: you can redistribute it and/or modify | 
					
						
							|  |  |  |  * it under the terms of the GNU Affero General Public License as | 
					
						
							|  |  |  |  * published by the Free Software Foundation, either version 3 of the | 
					
						
							|  |  |  |  * License, or (at your option) any later version. | 
					
						
							|  |  |  |  * | 
					
						
							|  |  |  |  * This program is distributed in the hope that it will be useful, | 
					
						
							|  |  |  |  * but WITHOUT ANY WARRANTY; without even the implied warranty of | 
					
						
							|  |  |  |  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | 
					
						
							|  |  |  |  * GNU Affero General Public License for more details. | 
					
						
							|  |  |  |  * | 
					
						
							|  |  |  |  * You should have received a copy of the GNU Affero General Public License | 
					
						
							|  |  |  |  * along with this program.  If not, see <https://www.gnu.org/licenses/>. | 
					
						
							|  |  |  |  */ | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-09-20 06:36:43 +02:00
										 |  |  | declare(strict_types=1); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-07-25 09:01:44 +02:00
										 |  |  | namespace FireflyIII\Support\Http\Api; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | use Carbon\Carbon; | 
					
						
							|  |  |  | use FireflyIII\Exceptions\FireflyException; | 
					
						
							| 
									
										
										
										
											2023-08-12 18:21:29 +02:00
										 |  |  | use FireflyIII\Models\CurrencyExchangeRate; | 
					
						
							| 
									
										
										
										
											2023-07-25 09:01:44 +02:00
										 |  |  | use FireflyIII\Models\TransactionCurrency; | 
					
						
							| 
									
										
										
										
											2023-08-12 18:21:29 +02:00
										 |  |  | use FireflyIII\Support\CacheProperties; | 
					
						
							| 
									
										
										
										
											2023-12-21 04:42:39 +01:00
										 |  |  | use Illuminate\Support\Facades\Log; | 
					
						
							| 
									
										
										
										
											2023-07-25 09:01:44 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | /** | 
					
						
							|  |  |  |  * Class ExchangeRateConverter | 
					
						
							|  |  |  |  */ | 
					
						
							|  |  |  | class ExchangeRateConverter | 
					
						
							|  |  |  | { | 
					
						
							| 
									
										
										
										
											2023-12-20 19:35:52 +01:00
										 |  |  |     // use ConvertsExchangeRates;
 | 
					
						
							| 
									
										
										
										
											2023-07-25 09:01:44 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-08-06 07:04:09 +02:00
										 |  |  |     /** | 
					
						
							|  |  |  |      * @throws FireflyException | 
					
						
							|  |  |  |      */ | 
					
						
							|  |  |  |     public function convert(TransactionCurrency $from, TransactionCurrency $to, Carbon $date, string $amount): string | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |         $rate = $this->getCurrencyRate($from, $to, $date); | 
					
						
							| 
									
										
										
										
											2023-12-20 19:35:52 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-08-06 07:04:09 +02:00
										 |  |  |         return bcmul($amount, $rate); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-07-25 09:01:44 +02:00
										 |  |  |     /** | 
					
						
							|  |  |  |      * @throws FireflyException | 
					
						
							|  |  |  |      */ | 
					
						
							|  |  |  |     public function getCurrencyRate(TransactionCurrency $from, TransactionCurrency $to, Carbon $date): string | 
					
						
							|  |  |  |     { | 
					
						
							| 
									
										
										
										
											2023-08-12 18:21:29 +02:00
										 |  |  |         $rate = $this->getRate($from, $to, $date); | 
					
						
							| 
									
										
										
										
											2023-12-20 19:35:52 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-08-12 18:21:29 +02:00
										 |  |  |         return '0' === $rate ? '1' : $rate; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     /** | 
					
						
							|  |  |  |      * @throws FireflyException | 
					
						
							|  |  |  |      */ | 
					
						
							|  |  |  |     private function getRate(TransactionCurrency $from, TransactionCurrency $to, Carbon $date): string | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |         // first attempt:
 | 
					
						
							| 
									
										
										
										
											2023-11-05 19:41:37 +01:00
										 |  |  |         $rate = $this->getFromDB($from->id, $to->id, $date->format('Y-m-d')); | 
					
						
							| 
									
										
										
										
											2023-08-12 18:21:29 +02:00
										 |  |  |         if (null !== $rate) { | 
					
						
							|  |  |  |             return $rate; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |         // no result. perhaps the other way around?
 | 
					
						
							| 
									
										
										
										
											2023-11-05 19:41:37 +01:00
										 |  |  |         $rate = $this->getFromDB($to->id, $from->id, $date->format('Y-m-d')); | 
					
						
							| 
									
										
										
										
											2023-08-12 18:21:29 +02:00
										 |  |  |         if (null !== $rate) { | 
					
						
							|  |  |  |             return bcdiv('1', $rate); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         // if nothing in place, fall back on the rate for $from to EUR
 | 
					
						
							|  |  |  |         $first  = $this->getEuroRate($from, $date); | 
					
						
							|  |  |  |         $second = $this->getEuroRate($to, $date); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         // combined (if present), they can be used to calculate the necessary conversion rate.
 | 
					
						
							| 
									
										
										
										
											2023-12-21 04:42:39 +01:00
										 |  |  |         if (0 === bccomp('0', $first) || 0 === bccomp('0', $second)) { | 
					
						
							|  |  |  |             Log::warning(sprintf('$first is "%s" and $second is "%s"', $first, $second)); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-08-12 18:21:29 +02:00
										 |  |  |             return '0'; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         $second = bcdiv('1', $second); | 
					
						
							| 
									
										
										
										
											2023-12-20 19:35:52 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-08-12 18:21:29 +02:00
										 |  |  |         return bcmul($first, $second); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     private function getFromDB(int $from, int $to, string $date): ?string | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |         $key = sprintf('cer-%d-%d-%s', $from, $to, $date); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         $cache = new CacheProperties(); | 
					
						
							|  |  |  |         $cache->addProperty($key); | 
					
						
							|  |  |  |         if ($cache->has()) { | 
					
						
							|  |  |  |             $rate = $cache->get(); | 
					
						
							|  |  |  |             if ('' === $rate) { | 
					
						
							|  |  |  |                 return null; | 
					
						
							|  |  |  |             } | 
					
						
							| 
									
										
										
										
											2023-12-20 19:35:52 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-08-12 18:21:29 +02:00
										 |  |  |             return $rate; | 
					
						
							| 
									
										
										
										
											2023-07-25 09:01:44 +02:00
										 |  |  |         } | 
					
						
							| 
									
										
										
										
											2023-08-12 18:21:29 +02:00
										 |  |  |         app('log')->debug(sprintf('Going to get rate #%d->#%d (%s) from DB.', $from, $to, $date)); | 
					
						
							| 
									
										
										
										
											2023-07-25 09:01:44 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-12-20 19:35:52 +01:00
										 |  |  |         /** @var null|CurrencyExchangeRate $result */ | 
					
						
							| 
									
										
										
										
											2023-08-12 18:21:29 +02:00
										 |  |  |         $result = auth()->user() | 
					
						
							| 
									
										
										
										
											2023-12-20 19:35:52 +01:00
										 |  |  |             ->currencyExchangeRates() | 
					
						
							|  |  |  |             ->where('from_currency_id', $from) | 
					
						
							|  |  |  |             ->where('to_currency_id', $to) | 
					
						
							|  |  |  |             ->where('date', '<=', $date) | 
					
						
							|  |  |  |             ->orderBy('date', 'DESC') | 
					
						
							|  |  |  |             ->first() | 
					
						
							|  |  |  |         ; | 
					
						
							| 
									
										
										
										
											2023-12-21 04:42:39 +01:00
										 |  |  |         $rate   = (string) $result?->rate; | 
					
						
							| 
									
										
										
										
											2023-08-12 18:21:29 +02:00
										 |  |  |         $cache->store($rate); | 
					
						
							|  |  |  |         if ('' === $rate) { | 
					
						
							|  |  |  |             return null; | 
					
						
							|  |  |  |         } | 
					
						
							| 
									
										
										
										
											2023-12-20 19:35:52 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-08-12 18:21:29 +02:00
										 |  |  |         return $rate; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     /** | 
					
						
							|  |  |  |      * @throws FireflyException | 
					
						
							|  |  |  |      */ | 
					
						
							|  |  |  |     private function getEuroRate(TransactionCurrency $currency, Carbon $date): string | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |         $euroId = $this->getEuroId(); | 
					
						
							| 
									
										
										
										
											2023-11-05 19:41:37 +01:00
										 |  |  |         if ($euroId === $currency->id) { | 
					
						
							| 
									
										
										
										
											2023-07-25 09:01:44 +02:00
										 |  |  |             return '1'; | 
					
						
							|  |  |  |         } | 
					
						
							| 
									
										
										
										
											2023-11-05 19:41:37 +01:00
										 |  |  |         $rate = $this->getFromDB($currency->id, $euroId, $date->format('Y-m-d')); | 
					
						
							| 
									
										
										
										
											2023-07-25 09:01:44 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-08-12 18:21:29 +02:00
										 |  |  |         if (null !== $rate) { | 
					
						
							|  |  |  |             //            app('log')->debug(sprintf('Rate for %s to EUR is %s.', $currency->code, $rate));
 | 
					
						
							|  |  |  |             return $rate; | 
					
						
							|  |  |  |         } | 
					
						
							| 
									
										
										
										
											2023-11-05 19:41:37 +01:00
										 |  |  |         $rate = $this->getFromDB($euroId, $currency->id, $date->format('Y-m-d')); | 
					
						
							| 
									
										
										
										
											2023-08-12 18:21:29 +02:00
										 |  |  |         if (null !== $rate) { | 
					
						
							|  |  |  |             return bcdiv('1', $rate); | 
					
						
							|  |  |  |             //            app('log')->debug(sprintf('Inverted rate for %s to EUR is %s.', $currency->code, $rate));
 | 
					
						
							| 
									
										
										
										
											2023-12-20 19:35:52 +01:00
										 |  |  |             // return $rate;
 | 
					
						
							| 
									
										
										
										
											2023-08-12 18:21:29 +02:00
										 |  |  |         } | 
					
						
							|  |  |  |         // grab backup values from config file:
 | 
					
						
							|  |  |  |         $backup = config(sprintf('cer.rates.%s', $currency->code)); | 
					
						
							|  |  |  |         if (null !== $backup) { | 
					
						
							| 
									
										
										
										
											2023-12-21 04:42:39 +01:00
										 |  |  |             return bcdiv('1', (string) $backup); | 
					
						
							| 
									
										
										
										
											2023-08-12 18:21:29 +02:00
										 |  |  |             // app('log')->debug(sprintf('Backup rate for %s to EUR is %s.', $currency->code, $backup));
 | 
					
						
							| 
									
										
										
										
											2023-12-20 19:35:52 +01:00
										 |  |  |             // return $backup;
 | 
					
						
							| 
									
										
										
										
											2023-08-12 18:21:29 +02:00
										 |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         //        app('log')->debug(sprintf('No rate for %s to EUR.', $currency->code));
 | 
					
						
							|  |  |  |         return '0'; | 
					
						
							| 
									
										
										
										
											2023-07-25 09:01:44 +02:00
										 |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-08-12 18:21:29 +02:00
										 |  |  |     /** | 
					
						
							|  |  |  |      * @throws FireflyException | 
					
						
							|  |  |  |      */ | 
					
						
							|  |  |  |     private function getEuroId(): int | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |         $cache = new CacheProperties(); | 
					
						
							|  |  |  |         $cache->addProperty('cer-euro-id'); | 
					
						
							|  |  |  |         if ($cache->has()) { | 
					
						
							| 
									
										
										
										
											2023-12-21 04:42:39 +01:00
										 |  |  |             return (int) $cache->get(); | 
					
						
							| 
									
										
										
										
											2023-08-12 18:21:29 +02:00
										 |  |  |         } | 
					
						
							|  |  |  |         $euro = TransactionCurrency::whereCode('EUR')->first(); | 
					
						
							|  |  |  |         if (null === $euro) { | 
					
						
							|  |  |  |             throw new FireflyException('Cannot find EUR in system, cannot do currency conversion.'); | 
					
						
							|  |  |  |         } | 
					
						
							| 
									
										
										
										
											2023-11-05 19:41:37 +01:00
										 |  |  |         $cache->store($euro->id); | 
					
						
							| 
									
										
										
										
											2023-12-20 19:35:52 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-11-05 19:41:37 +01:00
										 |  |  |         return $euro->id; | 
					
						
							| 
									
										
										
										
											2023-08-12 18:21:29 +02:00
										 |  |  |     } | 
					
						
							| 
									
										
										
										
											2023-07-25 09:01:44 +02:00
										 |  |  | } |