diff --git a/.env.example b/.env.example index 4124a82..07684f0 100644 --- a/.env.example +++ b/.env.example @@ -55,6 +55,8 @@ MAIL_USERNAME=null MAIL_PASSWORD=null MAIL_FROM_ADDRESS="hello@example.com" MAIL_FROM_NAME="${APP_NAME}" +MAIL_ADMIN_ADDRESS="watheq.alshowaiter@gmail.com" +MAIL_ADMIN_NAME="Watheq Alshowaiter" AWS_ACCESS_KEY_ID= AWS_SECRET_ACCESS_KEY= diff --git a/README.md b/README.md index 0aac7c0..6931f69 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,8 @@ to be added ## Plan - [x] Add html view to show the currency data - [x] Save command data to database +- [ ] email when failing fetching currency using resen +- [ ] caching for fetching command (6 hours), and for the controller (1 hour) - [ ] make proper pagination - [ ] save tabs state as query param - [ ] make a complete test for the project diff --git a/app/Console/Commands/FetchCurrencyCommand.php b/app/Console/Commands/FetchCurrencyCommand.php index 92bc576..d05a422 100644 --- a/app/Console/Commands/FetchCurrencyCommand.php +++ b/app/Console/Commands/FetchCurrencyCommand.php @@ -41,9 +41,9 @@ class FetchCurrencyCommand extends Command 'SAR' => Currency::where('code', 'SAR')->first(), }; - $rate = Rate::updateOrCreate([ - 'city_id' => $city->id, - 'currency_id' => $currency->id, + Rate::updateOrCreate([ + 'city_id' => $city?->id, + 'currency_id' => $currency?->id, 'date' => $item['date'], ], [ 'buy_price' => $item['price_buy'], @@ -61,6 +61,8 @@ class FetchCurrencyCommand extends Command 'trace' => $e->getTraceAsString(), ]); + // todo mail when failing using resend + return Command::FAILURE; } } @@ -110,7 +112,7 @@ class FetchCurrencyCommand extends Command if (empty($data)) { $this->warn('No historical currency data available.'); - return; + return []; } $this->info('Last 20 Days Currency Data:'); diff --git a/app/Http/Controllers/RateController.php b/app/Http/Controllers/RateController.php index 1f49baf..a208c58 100644 --- a/app/Http/Controllers/RateController.php +++ b/app/Http/Controllers/RateController.php @@ -4,31 +4,16 @@ namespace App\Http\Controllers; use App\Models\City; use App\Models\Rate; +use App\Services\RateService; class RateController extends Controller { - public function __invoke() + public function __invoke( RateService $rateService) { + $rates = $rateService->getFormattedRates(); - $supportedCities = City::query() - ->whereIn('name', City::supportedCities()) - ->get() - ->sortByDesc(fn ($city) => $city->name === 'sanaa') - ->values(); - - $rates = Rate::query() - ->with('currency', 'city') - ->orderBy('city_id', 'asc') - ->orderBy('currency_id', 'asc') - ->orderBy('date', 'desc') - ->get() - ->groupBy(fn ($rate) => $rate->city_id.'-'.$rate->currency_id) - ->map(fn ($group) => $group->first()) - ->groupBy('city_id'); - - return view('rates', [ - 'rates' => $rates, - 'supportedCities' => $supportedCities, + return view('rates', [ + 'rates' => $rates, ]); } } diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 452e6b6..c78051a 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -2,6 +2,11 @@ namespace App\Providers; +use Carbon\CarbonImmutable; +use Illuminate\Database\Eloquent\Model; +use Illuminate\Support\Facades\Date; +use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\URL; use Illuminate\Support\ServiceProvider; class AppServiceProvider extends ServiceProvider @@ -19,6 +24,12 @@ class AppServiceProvider extends ServiceProvider */ public function boot(): void { - // + DB::prohibitDestructiveCommands(app()->isProduction()); + + URL::forceHttps(app()->isProduction()); + + Model::shouldBeStrict(! app()->isProduction()); + + Date::use(CarbonImmutable::class); } } diff --git a/app/Services/RateService.php b/app/Services/RateService.php new file mode 100644 index 0000000..7882d25 --- /dev/null +++ b/app/Services/RateService.php @@ -0,0 +1,51 @@ +whereIn('name', City::supportedCities()) // Filter only supported cities + ->get() + ->sortByDesc(fn($city) => $city->name === 'sanaa') // Sort to put 'sanaa' first + ->values(); // Reset array keys to be sequential + + // Get the latest exchange rates for each currency in each city + $latestRates = Rate::query() + ->with('currency', 'city') + ->orderBy('city_id', 'asc') + ->orderBy('currency_id', 'asc') + ->orderBy('date', 'desc') + ->get() + ->groupBy(fn($rate) => $rate->city_id . '-' . $rate->currency_id) + ->map(fn($group) => $group->first()) + ->groupBy('city_id'); + + + // Format the rates + $rates = $supportedCities->map(function ($city) use ($latestRates) { + $cityRates = $latestRates->get($city->id, collect())->map(function ($rate) { + return [ + 'currency' => $rate->currency->name, + 'buy_price' => $rate->buy_price, + 'sell_price' => $rate->sell_price, + 'date' => $rate->date->toDateString(), + 'day' => $rate->date->locale('ar')->isoFormat('dddd'), + 'last_update' => $rate->updated_at->diffForHumans(), + ]; + })->values()->toArray(); + + return [ + 'city' => $city->label , + 'rates' => $cityRates, + ]; + })->toArray(); + + return $rates; + } +} diff --git a/config/mail.php b/config/mail.php index 0034532..ca938c6 100644 --- a/config/mail.php +++ b/config/mail.php @@ -115,4 +115,20 @@ return [ 'name' => env('MAIL_FROM_NAME', 'Example'), ], + /* + |-------------------------------------------------------------------------- + | Global "Admin" Email Address + |-------------------------------------------------------------------------- + | + | This address is used for sending important system alerts or notifications + | to the administrator. It can be referenced across the app when notifying + | the site admin about system-level issues. + | + */ + + 'admin' => [ + 'address' => env('MAIL_ADMIN_ADDRESS'), + 'name' => env('MAIL_ADMIN_NAME', 'Admin'), + ] + ]; diff --git a/database/factories/CityFactory.php b/database/factories/CityFactory.php index 36a420e..90a552f 100644 --- a/database/factories/CityFactory.php +++ b/database/factories/CityFactory.php @@ -19,6 +19,14 @@ class CityFactory extends Factory { return [ 'name' => fake()->randomElement(City::supportedCities()), + + 'label' => function ($attributes) { + return match ($attributes['name']) { + City::SANAA => 'صنعاء', + City::ADEN => 'عدن', + default => $attributes['name'], + }; + }, ]; } @@ -26,7 +34,6 @@ class CityFactory extends Factory { return $this->state([ 'name' => City::SANAA, - 'label' => 'صنعاء', ]); } @@ -34,7 +41,6 @@ class CityFactory extends Factory { return $this->state([ 'name' => City::ADEN, - 'label' => 'عدن', ]); } } diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index 4860bc5..4bfc25e 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -16,18 +16,13 @@ class DatabaseSeeder extends Seeder public function run(): void { // currencies - $saudiRial = Currency::factory()->saudiRial()->create(); $usdDollar = Currency::factory()->usdDollar()->create(); + $saudiRial = Currency::factory()->saudiRial()->create(); // cities $sanaa = City::factory()->sanaa()->create(); $aden = City::factory()->aden()->create(); - // rates - Rate::factory()->recycle($usdDollar)->recycle($sanaa)->create(); - Rate::factory()->recycle($usdDollar)->recycle($aden)->create(); - - Rate::factory()->recycle($saudiRial)->recycle($sanaa)->create(); - Rate::factory()->recycle($saudiRial)->recycle($aden)->create(); + // we will seed rates with commands instead } } diff --git a/resources/views/rates.blade.php b/resources/views/rates.blade.php index cb1479f..f2dd994 100644 --- a/resources/views/rates.blade.php +++ b/resources/views/rates.blade.php @@ -100,49 +100,39 @@