feat: implement currency exchange rate scraping service and command
هذا الالتزام موجود في:
102
app/Console/Commands/FetchCurrencyCommand.php
Normal file
102
app/Console/Commands/FetchCurrencyCommand.php
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use App\Services\CurrencyService;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
|
class FetchCurrencyCommand extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'currency:fetch {--today : Fetch only today\'s data}';
|
||||||
|
|
||||||
|
protected $description = 'Fetch currency exchange rates from boqash.com';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the console command.
|
||||||
|
*
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
public function handle(CurrencyService $currencyService) : int
|
||||||
|
{
|
||||||
|
$this->info('Fetching currency data...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
if ($this->option('today')) {
|
||||||
|
$this->fetchTodayData($currencyService);
|
||||||
|
} else {
|
||||||
|
$this->fetchHistoricalData($currencyService);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->info('Currency data fetched successfully!');
|
||||||
|
return Command::SUCCESS;
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->error('Failed to fetch currency data: ' . $e->getMessage());
|
||||||
|
Log::error('Currency fetch command failed', [
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
'trace' => $e->getTraceAsString()
|
||||||
|
]);
|
||||||
|
return Command::FAILURE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch and display today's currency data.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
protected function fetchTodayData(CurrencyService $currencyService)
|
||||||
|
{
|
||||||
|
$data = $currencyService->getTodayCurrencies();
|
||||||
|
|
||||||
|
if (empty($data)) {
|
||||||
|
$this->warn('No currency data available for today.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->info('Today\'s Currency Data:');
|
||||||
|
$this->table(
|
||||||
|
['Currency', 'City', 'Buy Price', 'Sell Price', 'Date', 'Day'],
|
||||||
|
array_map(function ($item) {
|
||||||
|
return [
|
||||||
|
$item['currency'],
|
||||||
|
$item['city'],
|
||||||
|
$item['price_buy'],
|
||||||
|
$item['price_sell'],
|
||||||
|
$item['date'],
|
||||||
|
$item['day']
|
||||||
|
];
|
||||||
|
}, $data)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch and display historical currency data.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
protected function fetchHistoricalData(CurrencyService $currencyService)
|
||||||
|
{
|
||||||
|
$data = $currencyService->getLastTwentyDays();
|
||||||
|
|
||||||
|
if (empty($data)) {
|
||||||
|
$this->warn('No historical currency data available.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->info('Last 20 Days Currency Data:');
|
||||||
|
$this->table(
|
||||||
|
['Currency', 'City', 'Buy Price', 'Sell Price', 'Date', 'Day'],
|
||||||
|
array_map(function ($item) {
|
||||||
|
return [
|
||||||
|
$item['currency'],
|
||||||
|
$item['city'],
|
||||||
|
$item['price_buy'],
|
||||||
|
$item['price_sell'],
|
||||||
|
$item['date'],
|
||||||
|
$item['day']
|
||||||
|
];
|
||||||
|
}, $data)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
177
app/Services/CurrencyService.php
Normal file
177
app/Services/CurrencyService.php
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use Symfony\Component\BrowserKit\HttpBrowser;
|
||||||
|
use Symfony\Component\DomCrawler\Crawler;
|
||||||
|
|
||||||
|
class CurrencyService
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The URL to scrape currency data from
|
||||||
|
*/
|
||||||
|
protected string $url = 'https://boqash.com/price-currency/';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cities to filter by
|
||||||
|
*/
|
||||||
|
protected array $cities = ['Sanaa', 'Aden'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cache TTL in minutes
|
||||||
|
*
|
||||||
|
* 5 minutes less than 6 hours to avoid cache conflicts with schedule command every six hours
|
||||||
|
*/
|
||||||
|
protected int $cacheTtl = 350;
|
||||||
|
|
||||||
|
public function getLastTwentyDays(): array
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$client = new HttpBrowser();
|
||||||
|
$crawler = $client->request('GET', $this->url);
|
||||||
|
$table = $crawler->filter('table')->first();
|
||||||
|
$rows = $table->filter('tbody')->filter('tr');
|
||||||
|
|
||||||
|
$results = [];
|
||||||
|
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
$rowCrawler = new Crawler($row);
|
||||||
|
$cells = $rowCrawler->filter('td');
|
||||||
|
|
||||||
|
if ($cells->count() >= 5) {
|
||||||
|
$currency = $this->getCurrency($cells->eq(0)->text());
|
||||||
|
$city = $this->getCity($cells->eq(1)->text());
|
||||||
|
$priceBuy = $this->getPrice($cells->eq(2)->text());
|
||||||
|
$priceSell = $this->getPrice($cells->eq(3)->text());
|
||||||
|
$date = $this->getDate($cells->eq(4)->text());
|
||||||
|
|
||||||
|
$results[] = [
|
||||||
|
'currency' => $currency,
|
||||||
|
'city' => $city,
|
||||||
|
'price_buy' => $priceBuy,
|
||||||
|
'price_sell' => $priceSell,
|
||||||
|
'date' => $date->format('Y-m-d'),
|
||||||
|
'day' => $date->format('l'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $results;
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('Failed to fetch currency data for last twenty days', [
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
'trace' => $e->getTraceAsString()
|
||||||
|
]);
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get today's currency data for Sanaa and Aden
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function getTodayCurrencies(): array
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$allData = $this->getLastTwentyDays();
|
||||||
|
|
||||||
|
$today = Carbon::today();
|
||||||
|
|
||||||
|
// Filter for today's data and specific cities
|
||||||
|
$todayData = array_filter($allData, function ($item) use ($today) {
|
||||||
|
return $item['date'] === $today->format('Y-m-d') && in_array($item['city'], $this->cities);
|
||||||
|
});
|
||||||
|
|
||||||
|
// If no data for today, get the most recent data
|
||||||
|
if (empty($todayData)) {
|
||||||
|
// Sort by date descending
|
||||||
|
usort($allData, function ($a, $b) {
|
||||||
|
return strtotime($b['date']) - strtotime($a['date']);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get the most recent date
|
||||||
|
$mostRecentDate = !empty($allData) ? $allData[0]['date'] : null;
|
||||||
|
|
||||||
|
if ($mostRecentDate) {
|
||||||
|
$todayData = array_filter($allData, function ($item) use ($mostRecentDate) {
|
||||||
|
return $item['date'] === $mostRecentDate && in_array($item['city'], $this->cities);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_values($todayData);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('Failed to fetch today\'s currency data', [
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
'trace' => $e->getTraceAsString()
|
||||||
|
]);
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert currency text to code
|
||||||
|
*
|
||||||
|
* @param string $currencyText
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
private function getCurrency(string $currencyText): string
|
||||||
|
{
|
||||||
|
$currency = trim($currencyText);
|
||||||
|
|
||||||
|
return match ($currency) {
|
||||||
|
'دولار أمريكي' => 'USD',
|
||||||
|
'ريال سعودي' => 'SAR',
|
||||||
|
default => $currency
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean and return city name
|
||||||
|
*
|
||||||
|
* @param string $cityText
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
private function getCity(string $cityText): string
|
||||||
|
{
|
||||||
|
$city = trim($cityText);
|
||||||
|
|
||||||
|
return match ($city) {
|
||||||
|
'صنعاء' => "Sanaa",
|
||||||
|
'عدن' => 'Aden',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract and clean price value
|
||||||
|
*
|
||||||
|
* @param string $priceText
|
||||||
|
* @return float
|
||||||
|
*/
|
||||||
|
private function getPrice(string $priceText): float
|
||||||
|
{
|
||||||
|
// Remove special characters
|
||||||
|
$priceText = str_replace(['●', '▲', '▼', 'ريال', ','], '', $priceText);
|
||||||
|
|
||||||
|
// Extract only numbers and decimal point
|
||||||
|
$priceText = preg_replace('/[^0-9.]/', '', $priceText);
|
||||||
|
|
||||||
|
return (float) $priceText;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse date text to Carbon instance
|
||||||
|
*
|
||||||
|
* @param string $dateText
|
||||||
|
* @return Carbon
|
||||||
|
*/
|
||||||
|
private function getDate(string $dateText): Carbon
|
||||||
|
{
|
||||||
|
return Carbon::parse($dateText);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,7 +8,9 @@
|
|||||||
"require": {
|
"require": {
|
||||||
"php": "^8.2",
|
"php": "^8.2",
|
||||||
"laravel/framework": "^12.0",
|
"laravel/framework": "^12.0",
|
||||||
"laravel/tinker": "^2.10.1"
|
"laravel/tinker": "^2.10.1",
|
||||||
|
"symfony/browser-kit": "^7.2",
|
||||||
|
"symfony/http-client": "^7.2"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"fakerphp/faker": "^1.23",
|
"fakerphp/faker": "^1.23",
|
||||||
|
|||||||
377
composer.lock
مولّد
377
composer.lock
مولّد
@@ -4,7 +4,7 @@
|
|||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "88970a0117c062eed55fa8728fc43833",
|
"content-hash": "b5ccaeb33793a0fd614677d4cdb3d4c9",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "brick/math",
|
"name": "brick/math",
|
||||||
@@ -2006,6 +2006,73 @@
|
|||||||
],
|
],
|
||||||
"time": "2024-12-08T08:18:47+00:00"
|
"time": "2024-12-08T08:18:47+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "masterminds/html5",
|
||||||
|
"version": "2.9.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/Masterminds/html5-php.git",
|
||||||
|
"reference": "f5ac2c0b0a2eefca70b2ce32a5809992227e75a6"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/Masterminds/html5-php/zipball/f5ac2c0b0a2eefca70b2ce32a5809992227e75a6",
|
||||||
|
"reference": "f5ac2c0b0a2eefca70b2ce32a5809992227e75a6",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"ext-dom": "*",
|
||||||
|
"php": ">=5.3.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"phpunit/phpunit": "^4.8.35 || ^5.7.21 || ^6 || ^7 || ^8 || ^9"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"extra": {
|
||||||
|
"branch-alias": {
|
||||||
|
"dev-master": "2.7-dev"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Masterminds\\": "src"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Matt Butcher",
|
||||||
|
"email": "technosophos@gmail.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Matt Farina",
|
||||||
|
"email": "matt@mattfarina.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Asmir Mustafic",
|
||||||
|
"email": "goetas@gmail.com"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "An HTML5 parser and serializer.",
|
||||||
|
"homepage": "http://masterminds.github.io/html5-php",
|
||||||
|
"keywords": [
|
||||||
|
"HTML5",
|
||||||
|
"dom",
|
||||||
|
"html",
|
||||||
|
"parser",
|
||||||
|
"querypath",
|
||||||
|
"serializer",
|
||||||
|
"xml"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/Masterminds/html5-php/issues",
|
||||||
|
"source": "https://github.com/Masterminds/html5-php/tree/2.9.0"
|
||||||
|
},
|
||||||
|
"time": "2024-03-31T07:05:07+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "monolog/monolog",
|
"name": "monolog/monolog",
|
||||||
"version": "3.9.0",
|
"version": "3.9.0",
|
||||||
@@ -3286,6 +3353,74 @@
|
|||||||
],
|
],
|
||||||
"time": "2024-04-27T21:32:50+00:00"
|
"time": "2024-04-27T21:32:50+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "symfony/browser-kit",
|
||||||
|
"version": "v7.2.4",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/symfony/browser-kit.git",
|
||||||
|
"reference": "8ce0ee23857d87d5be493abba2d52d1f9e49da61"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/symfony/browser-kit/zipball/8ce0ee23857d87d5be493abba2d52d1f9e49da61",
|
||||||
|
"reference": "8ce0ee23857d87d5be493abba2d52d1f9e49da61",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": ">=8.2",
|
||||||
|
"symfony/dom-crawler": "^6.4|^7.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"symfony/css-selector": "^6.4|^7.0",
|
||||||
|
"symfony/http-client": "^6.4|^7.0",
|
||||||
|
"symfony/mime": "^6.4|^7.0",
|
||||||
|
"symfony/process": "^6.4|^7.0"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Symfony\\Component\\BrowserKit\\": ""
|
||||||
|
},
|
||||||
|
"exclude-from-classmap": [
|
||||||
|
"/Tests/"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Fabien Potencier",
|
||||||
|
"email": "fabien@symfony.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Symfony Community",
|
||||||
|
"homepage": "https://symfony.com/contributors"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Simulates the behavior of a web browser, allowing you to make requests, click on links and submit forms programmatically",
|
||||||
|
"homepage": "https://symfony.com",
|
||||||
|
"support": {
|
||||||
|
"source": "https://github.com/symfony/browser-kit/tree/v7.2.4"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://symfony.com/sponsor",
|
||||||
|
"type": "custom"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/fabpot",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||||
|
"type": "tidelift"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2025-02-14T14:27:24+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/clock",
|
"name": "symfony/clock",
|
||||||
"version": "v7.2.0",
|
"version": "v7.2.0",
|
||||||
@@ -3585,6 +3720,73 @@
|
|||||||
],
|
],
|
||||||
"time": "2024-09-25T14:20:29+00:00"
|
"time": "2024-09-25T14:20:29+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "symfony/dom-crawler",
|
||||||
|
"version": "v7.2.4",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/symfony/dom-crawler.git",
|
||||||
|
"reference": "19cc7b08efe9ad1ab1b56e0948e8d02e15ed3ef7"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/symfony/dom-crawler/zipball/19cc7b08efe9ad1ab1b56e0948e8d02e15ed3ef7",
|
||||||
|
"reference": "19cc7b08efe9ad1ab1b56e0948e8d02e15ed3ef7",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"masterminds/html5": "^2.6",
|
||||||
|
"php": ">=8.2",
|
||||||
|
"symfony/polyfill-ctype": "~1.8",
|
||||||
|
"symfony/polyfill-mbstring": "~1.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"symfony/css-selector": "^6.4|^7.0"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Symfony\\Component\\DomCrawler\\": ""
|
||||||
|
},
|
||||||
|
"exclude-from-classmap": [
|
||||||
|
"/Tests/"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Fabien Potencier",
|
||||||
|
"email": "fabien@symfony.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Symfony Community",
|
||||||
|
"homepage": "https://symfony.com/contributors"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Eases DOM navigation for HTML and XML documents",
|
||||||
|
"homepage": "https://symfony.com",
|
||||||
|
"support": {
|
||||||
|
"source": "https://github.com/symfony/dom-crawler/tree/v7.2.4"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://symfony.com/sponsor",
|
||||||
|
"type": "custom"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/fabpot",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||||
|
"type": "tidelift"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2025-02-17T15:53:07+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/error-handler",
|
"name": "symfony/error-handler",
|
||||||
"version": "v7.2.5",
|
"version": "v7.2.5",
|
||||||
@@ -3880,6 +4082,179 @@
|
|||||||
],
|
],
|
||||||
"time": "2024-12-30T19:00:17+00:00"
|
"time": "2024-12-30T19:00:17+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "symfony/http-client",
|
||||||
|
"version": "v7.2.4",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/symfony/http-client.git",
|
||||||
|
"reference": "78981a2ffef6437ed92d4d7e2a86a82f256c6dc6"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/symfony/http-client/zipball/78981a2ffef6437ed92d4d7e2a86a82f256c6dc6",
|
||||||
|
"reference": "78981a2ffef6437ed92d4d7e2a86a82f256c6dc6",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": ">=8.2",
|
||||||
|
"psr/log": "^1|^2|^3",
|
||||||
|
"symfony/deprecation-contracts": "^2.5|^3",
|
||||||
|
"symfony/http-client-contracts": "~3.4.4|^3.5.2",
|
||||||
|
"symfony/service-contracts": "^2.5|^3"
|
||||||
|
},
|
||||||
|
"conflict": {
|
||||||
|
"amphp/amp": "<2.5",
|
||||||
|
"php-http/discovery": "<1.15",
|
||||||
|
"symfony/http-foundation": "<6.4"
|
||||||
|
},
|
||||||
|
"provide": {
|
||||||
|
"php-http/async-client-implementation": "*",
|
||||||
|
"php-http/client-implementation": "*",
|
||||||
|
"psr/http-client-implementation": "1.0",
|
||||||
|
"symfony/http-client-implementation": "3.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"amphp/http-client": "^4.2.1|^5.0",
|
||||||
|
"amphp/http-tunnel": "^1.0|^2.0",
|
||||||
|
"amphp/socket": "^1.1",
|
||||||
|
"guzzlehttp/promises": "^1.4|^2.0",
|
||||||
|
"nyholm/psr7": "^1.0",
|
||||||
|
"php-http/httplug": "^1.0|^2.0",
|
||||||
|
"psr/http-client": "^1.0",
|
||||||
|
"symfony/amphp-http-client-meta": "^1.0|^2.0",
|
||||||
|
"symfony/dependency-injection": "^6.4|^7.0",
|
||||||
|
"symfony/http-kernel": "^6.4|^7.0",
|
||||||
|
"symfony/messenger": "^6.4|^7.0",
|
||||||
|
"symfony/process": "^6.4|^7.0",
|
||||||
|
"symfony/rate-limiter": "^6.4|^7.0",
|
||||||
|
"symfony/stopwatch": "^6.4|^7.0"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Symfony\\Component\\HttpClient\\": ""
|
||||||
|
},
|
||||||
|
"exclude-from-classmap": [
|
||||||
|
"/Tests/"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Nicolas Grekas",
|
||||||
|
"email": "p@tchwork.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Symfony Community",
|
||||||
|
"homepage": "https://symfony.com/contributors"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Provides powerful methods to fetch HTTP resources synchronously or asynchronously",
|
||||||
|
"homepage": "https://symfony.com",
|
||||||
|
"keywords": [
|
||||||
|
"http"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"source": "https://github.com/symfony/http-client/tree/v7.2.4"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://symfony.com/sponsor",
|
||||||
|
"type": "custom"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/fabpot",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||||
|
"type": "tidelift"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2025-02-13T10:27:23+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "symfony/http-client-contracts",
|
||||||
|
"version": "v3.5.2",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/symfony/http-client-contracts.git",
|
||||||
|
"reference": "ee8d807ab20fcb51267fdace50fbe3494c31e645"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/ee8d807ab20fcb51267fdace50fbe3494c31e645",
|
||||||
|
"reference": "ee8d807ab20fcb51267fdace50fbe3494c31e645",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": ">=8.1"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"extra": {
|
||||||
|
"thanks": {
|
||||||
|
"url": "https://github.com/symfony/contracts",
|
||||||
|
"name": "symfony/contracts"
|
||||||
|
},
|
||||||
|
"branch-alias": {
|
||||||
|
"dev-main": "3.5-dev"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Symfony\\Contracts\\HttpClient\\": ""
|
||||||
|
},
|
||||||
|
"exclude-from-classmap": [
|
||||||
|
"/Test/"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Nicolas Grekas",
|
||||||
|
"email": "p@tchwork.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Symfony Community",
|
||||||
|
"homepage": "https://symfony.com/contributors"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Generic abstractions related to HTTP clients",
|
||||||
|
"homepage": "https://symfony.com",
|
||||||
|
"keywords": [
|
||||||
|
"abstractions",
|
||||||
|
"contracts",
|
||||||
|
"decoupling",
|
||||||
|
"interfaces",
|
||||||
|
"interoperability",
|
||||||
|
"standards"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"source": "https://github.com/symfony/http-client-contracts/tree/v3.5.2"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://symfony.com/sponsor",
|
||||||
|
"type": "custom"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/fabpot",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||||
|
"type": "tidelift"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2024-12-07T08:49:48+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/http-foundation",
|
"name": "symfony/http-foundation",
|
||||||
"version": "v7.2.6",
|
"version": "v7.2.6",
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
use Illuminate\Foundation\Inspiring;
|
use Illuminate\Foundation\Inspiring;
|
||||||
use Illuminate\Support\Facades\Artisan;
|
use Illuminate\Support\Facades\Artisan;
|
||||||
|
|
||||||
Artisan::command('inspire', function () {
|
Schedule::command('currency:fetch --today')
|
||||||
$this->comment(Inspiring::quote());
|
->everySixHours() //
|
||||||
})->purpose('Display an inspiring quote');
|
->appendOutputTo(storage_path('logs/currency-fetch.log'));
|
||||||
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace Tests\Feature;
|
|
||||||
|
|
||||||
// use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
||||||
use Tests\TestCase;
|
|
||||||
|
|
||||||
class ExampleTest extends TestCase
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* A basic test example.
|
|
||||||
*/
|
|
||||||
public function test_the_application_returns_a_successful_response(): void
|
|
||||||
{
|
|
||||||
$response = $this->get('/');
|
|
||||||
|
|
||||||
$response->assertStatus(200);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
80
tests/Feature/Services/CurrencyServiceFeatureTest.php
Normal file
80
tests/Feature/Services/CurrencyServiceFeatureTest.php
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature\Services;
|
||||||
|
|
||||||
|
use App\Services\CurrencyService;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Illuminate\Contracts\Container\BindingResolutionException;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
use Illuminate\Support\Facades\File;
|
||||||
|
use Symfony\Component\DomCrawler\Crawler;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @group real test - requires internet connection and maybe slow
|
||||||
|
*/
|
||||||
|
class CurrencyServiceFeatureTest extends TestCase
|
||||||
|
{
|
||||||
|
protected $currencyService;
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
|
||||||
|
$this->currencyService = new CurrencyService();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test getting last twenty days of currency data
|
||||||
|
*/
|
||||||
|
public function testGetLastTwentyDays()
|
||||||
|
{
|
||||||
|
$result = $this->currencyService->getLastTwentyDays();
|
||||||
|
|
||||||
|
$this->assertIsArray($result);
|
||||||
|
$this->assertNotEmpty($result);
|
||||||
|
|
||||||
|
$firstItem = $result[0];
|
||||||
|
$this->assertArrayHasKey('currency', $firstItem);
|
||||||
|
$this->assertArrayHasKey('city', $firstItem);
|
||||||
|
$this->assertArrayHasKey('price_buy', $firstItem);
|
||||||
|
$this->assertArrayHasKey('price_sell', $firstItem);
|
||||||
|
$this->assertArrayHasKey('date', $firstItem);
|
||||||
|
$this->assertArrayHasKey('day', $firstItem);
|
||||||
|
|
||||||
|
$this->assertContains('Sanaa', array_column($result, 'city'));
|
||||||
|
$this->assertContains('Aden', array_column($result, 'city'));
|
||||||
|
|
||||||
|
$this->assertContains('USD', array_column($result, 'currency'));
|
||||||
|
$this->assertContains('SAR', array_column($result, 'currency'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test getting today's currencies
|
||||||
|
*/
|
||||||
|
public function testGetTodayCurrencies()
|
||||||
|
{
|
||||||
|
$result = $this->currencyService->getTodayCurrencies();
|
||||||
|
|
||||||
|
// Assertions
|
||||||
|
$this->assertIsArray($result);
|
||||||
|
$this->assertNotEmpty($result);
|
||||||
|
|
||||||
|
// Should only return today's data for Sanaa and Aden
|
||||||
|
// In our sample, there are 4 entries for today (2 cities x 2 currencies)
|
||||||
|
$todayDate = Carbon::today()->format('Y-m-d');
|
||||||
|
$todayItems = array_filter($result, function ($item) use ($todayDate) {
|
||||||
|
return $item['date'] === $todayDate;
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// Check that we have both cities
|
||||||
|
$cities = array_column($todayItems, 'city');
|
||||||
|
$this->assertContains('Sanaa', $cities);
|
||||||
|
$this->assertContains('Aden', $cities);
|
||||||
|
|
||||||
|
// Check that we have both currencies
|
||||||
|
$currencies = array_column($todayItems, 'currency');
|
||||||
|
$this->assertContains('USD', $currencies);
|
||||||
|
$this->assertContains('SAR', $currencies);
|
||||||
|
}
|
||||||
|
}
|
||||||
205
tests/Unit/CurrencyServiceTest.php
Normal file
205
tests/Unit/CurrencyServiceTest.php
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Unit;
|
||||||
|
|
||||||
|
use App\Services\CurrencyService;
|
||||||
|
use Illuminate\Support\Carbon;
|
||||||
|
use Mockery;
|
||||||
|
use Symfony\Component\BrowserKit\HttpBrowser;
|
||||||
|
use Symfony\Component\DomCrawler\Crawler;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class CurrencyServiceTest extends TestCase
|
||||||
|
{
|
||||||
|
protected $currencyService;
|
||||||
|
protected $mockHttpBrowser;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
|
||||||
|
// Create a mock for HttpBrowser
|
||||||
|
$this->mockHttpBrowser = Mockery::mock(HttpBrowser::class);
|
||||||
|
|
||||||
|
// Create the service with mocked dependencies
|
||||||
|
$this->currencyService = Mockery::mock(CurrencyService::class)
|
||||||
|
->makePartial()
|
||||||
|
->shouldAllowMockingProtectedMethods();
|
||||||
|
|
||||||
|
// Configure the service to return our mock
|
||||||
|
$this->currencyService->shouldAllowMockingProtectedMethods();
|
||||||
|
|
||||||
|
$this->currencyService->shouldReceive('createHttpBrowser')
|
||||||
|
->andReturn($this->mockHttpBrowser);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetTodayCurrenciesFiltersForTodayAndCities()
|
||||||
|
{
|
||||||
|
// Sample data that includes today and past dates
|
||||||
|
$today = Carbon::today()->format('Y-m-d');
|
||||||
|
$yesterday = Carbon::yesterday()->format('Y-m-d');
|
||||||
|
|
||||||
|
$testData = [
|
||||||
|
[
|
||||||
|
'currency' => 'USD',
|
||||||
|
'city' => 'Sanaa',
|
||||||
|
'price_buy' => 535.0,
|
||||||
|
'price_sell' => 537.0,
|
||||||
|
'date' => $today,
|
||||||
|
'day' => Carbon::today()->format('l')
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'currency' => 'SAR',
|
||||||
|
'city' => 'Sanaa',
|
||||||
|
'price_buy' => 139.8,
|
||||||
|
'price_sell' => 140.2,
|
||||||
|
'date' => $today,
|
||||||
|
'day' => Carbon::today()->format('l')
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'currency' => 'USD',
|
||||||
|
'city' => 'Aden',
|
||||||
|
'price_buy' => 2541.0,
|
||||||
|
'price_sell' => 2556.0,
|
||||||
|
'date' => $today,
|
||||||
|
'day' => Carbon::today()->format('l')
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'currency' => 'SAR',
|
||||||
|
'city' => 'Aden',
|
||||||
|
'price_buy' => 668.0,
|
||||||
|
'price_sell' => 670.0,
|
||||||
|
'date' => $today,
|
||||||
|
'day' => Carbon::today()->format('l')
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'currency' => 'USD',
|
||||||
|
'city' => 'Sanaa',
|
||||||
|
'price_buy' => 534.0,
|
||||||
|
'price_sell' => 536.0,
|
||||||
|
'date' => $yesterday,
|
||||||
|
'day' => Carbon::yesterday()->format('l')
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'currency' => 'USD',
|
||||||
|
'city' => 'OtherCity',
|
||||||
|
'price_buy' => 540.0,
|
||||||
|
'price_sell' => 542.0,
|
||||||
|
'date' => $today,
|
||||||
|
'day' => Carbon::today()->format('l')
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
// Create a new instance for this test
|
||||||
|
$currencyService = Mockery::mock(CurrencyService::class)->makePartial();
|
||||||
|
$currencyService->shouldReceive('getLastTwentyDays')
|
||||||
|
->once()
|
||||||
|
->andReturn($testData);
|
||||||
|
|
||||||
|
// Test the method
|
||||||
|
$result = $currencyService->getTodayCurrencies();
|
||||||
|
|
||||||
|
// Should only include today's data for Sanaa and Aden
|
||||||
|
$this->assertCount(4, $result);
|
||||||
|
|
||||||
|
// Check that only Sanaa and Aden cities are included
|
||||||
|
$cities = array_column($result, 'city');
|
||||||
|
$this->assertContains('Sanaa', $cities);
|
||||||
|
$this->assertContains('Aden', $cities);
|
||||||
|
$this->assertNotContains('OtherCity', $cities);
|
||||||
|
|
||||||
|
// Check that only today's data is included
|
||||||
|
$dates = array_unique(array_column($result, 'date'));
|
||||||
|
$this->assertCount(1, $dates);
|
||||||
|
$this->assertEquals($today, $dates[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetTodayCurrenciesHandlesExceptions()
|
||||||
|
{
|
||||||
|
// Create a subclass of CurrencyService for testing
|
||||||
|
$currencyService = new class extends CurrencyService {
|
||||||
|
// Override getLastTwentyDays to throw an exception
|
||||||
|
public function getLastTwentyDays(): array
|
||||||
|
{
|
||||||
|
throw new \Exception('Test exception');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Test the method - this will call the real getTodayCurrencies which calls our overridden getLastTwentyDays
|
||||||
|
$result = $currencyService->getTodayCurrencies();
|
||||||
|
|
||||||
|
// Should return an empty array on exception
|
||||||
|
$this->assertIsArray($result);
|
||||||
|
$this->assertEmpty($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetTodayCurrenciesUsesRecentDataWhenTodayNotAvailable()
|
||||||
|
{
|
||||||
|
// Sample data that includes only past dates
|
||||||
|
$yesterday = Carbon::yesterday()->format('Y-m-d');
|
||||||
|
$twoDaysAgo = Carbon::yesterday()->subDay()->format('Y-m-d');
|
||||||
|
|
||||||
|
$testData = [
|
||||||
|
[
|
||||||
|
'currency' => 'USD',
|
||||||
|
'city' => 'Sanaa',
|
||||||
|
'price_buy' => 534.0,
|
||||||
|
'price_sell' => 536.0,
|
||||||
|
'date' => $yesterday,
|
||||||
|
'day' => Carbon::yesterday()->format('l')
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'currency' => 'SAR',
|
||||||
|
'city' => 'Sanaa',
|
||||||
|
'price_buy' => 139.0,
|
||||||
|
'price_sell' => 140.0,
|
||||||
|
'date' => $yesterday,
|
||||||
|
'day' => Carbon::yesterday()->format('l')
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'currency' => 'USD',
|
||||||
|
'city' => 'Aden',
|
||||||
|
'price_buy' => 2540.0,
|
||||||
|
'price_sell' => 2555.0,
|
||||||
|
'date' => $yesterday,
|
||||||
|
'day' => Carbon::yesterday()->format('l')
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'currency' => 'USD',
|
||||||
|
'city' => 'Sanaa',
|
||||||
|
'price_buy' => 533.0,
|
||||||
|
'price_sell' => 535.0,
|
||||||
|
'date' => $twoDaysAgo,
|
||||||
|
'day' => Carbon::yesterday()->subDay()->format('l')
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
// Create a new instance for this test
|
||||||
|
$currencyService = Mockery::mock(CurrencyService::class)->makePartial();
|
||||||
|
$currencyService->shouldReceive('getLastTwentyDays')
|
||||||
|
->once()
|
||||||
|
->andReturn($testData);
|
||||||
|
|
||||||
|
// Test the method
|
||||||
|
$result = $currencyService->getTodayCurrencies();
|
||||||
|
|
||||||
|
// Should include yesterday's data for Sanaa and Aden
|
||||||
|
$this->assertCount(3, $result);
|
||||||
|
|
||||||
|
// Check that only Sanaa and Aden cities are included
|
||||||
|
$cities = array_column($result, 'city');
|
||||||
|
$this->assertContains('Sanaa', $cities);
|
||||||
|
$this->assertContains('Aden', $cities);
|
||||||
|
|
||||||
|
// Check that only yesterday's data is included (most recent)
|
||||||
|
$dates = array_unique(array_column($result, 'date'));
|
||||||
|
$this->assertCount(1, $dates);
|
||||||
|
$this->assertEquals($yesterday, $dates[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function tearDown(): void
|
||||||
|
{
|
||||||
|
Mockery::close();
|
||||||
|
parent::tearDown();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace Tests\Unit;
|
|
||||||
|
|
||||||
use PHPUnit\Framework\TestCase;
|
|
||||||
|
|
||||||
class ExampleTest extends TestCase
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* A basic test example.
|
|
||||||
*/
|
|
||||||
public function test_that_true_is_true(): void
|
|
||||||
{
|
|
||||||
$this->assertTrue(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
140
tests/Unit/FetchCurrencyCommandTest.php
Normal file
140
tests/Unit/FetchCurrencyCommandTest.php
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Unit;
|
||||||
|
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
use App\Console\Commands\FetchCurrencyCommand;
|
||||||
|
use App\Services\CurrencyService;
|
||||||
|
use Mockery;
|
||||||
|
|
||||||
|
class FetchCurrencyCommandTest extends TestCase
|
||||||
|
{
|
||||||
|
protected $command;
|
||||||
|
protected $currencyService;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
|
||||||
|
// Create a mock for the CurrencyService
|
||||||
|
$this->currencyService = Mockery::mock(CurrencyService::class);
|
||||||
|
$this->app->instance(CurrencyService::class, $this->currencyService);
|
||||||
|
|
||||||
|
// Create the command
|
||||||
|
$this->command = new FetchCurrencyCommand();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testHandleWithTodayOption()
|
||||||
|
{
|
||||||
|
// Sample data for today's currencies
|
||||||
|
$todayCurrencies = [
|
||||||
|
[
|
||||||
|
'currency' => 'USD',
|
||||||
|
'city' => 'Sanaa',
|
||||||
|
'price_buy' => 535.0,
|
||||||
|
'price_sell' => 537.0,
|
||||||
|
'date' => '2025-05-19',
|
||||||
|
'day' => 'Monday'
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'currency' => 'SAR',
|
||||||
|
'city' => 'Sanaa',
|
||||||
|
'price_buy' => 139.8,
|
||||||
|
'price_sell' => 140.2,
|
||||||
|
'date' => '2025-05-19',
|
||||||
|
'day' => 'Monday'
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
// Configure the mock to return our sample data
|
||||||
|
$this->currencyService->shouldReceive('getTodayCurrencies')
|
||||||
|
->once()
|
||||||
|
->andReturn($todayCurrencies);
|
||||||
|
|
||||||
|
// We don't expect getLastTwentyDays to be called
|
||||||
|
$this->currencyService->shouldReceive('getLastTwentyDays')
|
||||||
|
->never();
|
||||||
|
|
||||||
|
// Create a mock for the command to capture output
|
||||||
|
$this->artisan('currency:fetch', ['--today' => true])
|
||||||
|
->expectsOutput('Fetching currency data...')
|
||||||
|
->expectsOutput('Today\'s Currency Data:')
|
||||||
|
->expectsOutput('Currency data fetched successfully!')
|
||||||
|
->assertExitCode(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testHandleWithoutTodayOption()
|
||||||
|
{
|
||||||
|
// Sample data for historical currencies
|
||||||
|
$historicalCurrencies = [
|
||||||
|
[
|
||||||
|
'currency' => 'USD',
|
||||||
|
'city' => 'Sanaa',
|
||||||
|
'price_buy' => 535.0,
|
||||||
|
'price_sell' => 537.0,
|
||||||
|
'date' => '2025-05-19',
|
||||||
|
'day' => 'Monday'
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'currency' => 'USD',
|
||||||
|
'city' => 'Sanaa',
|
||||||
|
'price_buy' => 534.0,
|
||||||
|
'price_sell' => 536.0,
|
||||||
|
'date' => '2025-05-18',
|
||||||
|
'day' => 'Sunday'
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
// Configure the mock to return our sample data
|
||||||
|
$this->currencyService->shouldReceive('getLastTwentyDays')
|
||||||
|
->once()
|
||||||
|
->andReturn($historicalCurrencies);
|
||||||
|
|
||||||
|
// We don't expect getTodayCurrencies to be called
|
||||||
|
$this->currencyService->shouldReceive('getTodayCurrencies')
|
||||||
|
->never();
|
||||||
|
|
||||||
|
// Create a mock for the command to capture output
|
||||||
|
$this->artisan('currency:fetch')
|
||||||
|
->expectsOutput('Fetching currency data...')
|
||||||
|
->expectsOutput('Last 20 Days Currency Data:')
|
||||||
|
->expectsOutput('Currency data fetched successfully!')
|
||||||
|
->assertExitCode(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testHandleWithEmptyData()
|
||||||
|
{
|
||||||
|
// Configure the mock to return empty data
|
||||||
|
$this->currencyService->shouldReceive('getLastTwentyDays')
|
||||||
|
->once()
|
||||||
|
->andReturn([]);
|
||||||
|
|
||||||
|
// Create a mock for the command to capture output
|
||||||
|
$this->artisan('currency:fetch')
|
||||||
|
->expectsOutput('Fetching currency data...')
|
||||||
|
->expectsOutput('No historical currency data available.')
|
||||||
|
->expectsOutput('Currency data fetched successfully!')
|
||||||
|
->assertExitCode(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testHandleWithException()
|
||||||
|
{
|
||||||
|
// Configure the mock to throw an exception
|
||||||
|
$this->currencyService->shouldReceive('getLastTwentyDays')
|
||||||
|
->once()
|
||||||
|
->andThrow(new \Exception('Test exception'));
|
||||||
|
|
||||||
|
// Create a mock for the command to capture output
|
||||||
|
$this->artisan('currency:fetch')
|
||||||
|
->expectsOutput('Fetching currency data...')
|
||||||
|
->expectsOutput('Failed to fetch currency data: Test exception')
|
||||||
|
->assertExitCode(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function tearDown(): void
|
||||||
|
{
|
||||||
|
Mockery::close();
|
||||||
|
parent::tearDown();
|
||||||
|
}
|
||||||
|
}
|
||||||
المرجع في مشكلة جديدة
حظر مستخدم