Currency Api Dashboard

TypeScript

Modern currency exchange rate dashboard with real-time market data, conversion tools, analytics, historical trends, and responsive admin interface.

Stars
18
Forks
1
Downloads
N/A
Open Issues
0
Files main

Repository Files

Loading file structure...
app/Http/Controllers/ApiController.php
<?php

namespace App\Http\Controllers;

use App\Models\Country;
use App\Models\Currency;
use App\Models\State;
use App\Models\Tokens;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;

class ApiController extends Controller
{
    /**
     * Authorize / Validate Token
     *
     * Check if a developer API key is valid and active.
     * Note: This endpoint requires the token to be provided in the Bearer Token request header.
     */
    public function authorize(Request $request): JsonResponse
    {
        $bearerToken = $request->bearerToken();
        if (! $bearerToken) {
            return response()->json([
                'status' => false,
                'message' => 'Token not provided.',
            ], 401);
        }

        $token = Tokens::where('token', $bearerToken)->where('active', true)->first();
        if (! $token) {
            return response()->json([
                'status' => false,
                'message' => 'Token is invalid or expired.',
            ], 401);
        }

        return response()->json([
            'status' => true,
            'message' => 'Token is valid.',
            'name' => $token->name,
            'default_currency' => $token->default_currency ?: 'INR',
        ]);
    }

    /**
     * Get Rates by Default Currency
     *
     * Retrieve all active currency rates relative to the API token's default currency.
     * If the token does not specify a default currency, the system fallback (INR) is used.
     */
    public function getCurrencies(Request $request): JsonResponse
    {
        $token = $request->attributes->get('api_token');
        $base = $token ? ($token->default_currency ?: 'INR') : 'INR';

        $currencies = Cache::remember('currencies:all_list', now()->addDays(7), function () {
            return Currency::where('active', true)->get()->keyBy('code')->toArray();
        });

        if (empty($currencies)) {
            Artisan::call('app:sync-currencies');
            Cache::forget('currencies:all_list');
            $currencies = Cache::remember('currencies:all_list', now()->addDays(7), function () {
                return Currency::where('active', true)->get()->keyBy('code')->toArray();
            });
        }

        if (empty($currencies)) {
            return response()->json([
                'status' => false,
                'message' => 'No active currency rates found in system.',
            ], 404);
        }

        $base = strtoupper($base);
        if (! isset($currencies[$base])) {
            $base = 'INR'; // fallback to INR
        }
        if (! isset($currencies[$base])) {
            $base = array_key_first($currencies);
        }

        $baseRate = $currencies[$base]['rate'];
        $convertedRates = [];

        foreach ($currencies as $code => $curr) {
            $convertedRates[$code] = [
                'code' => $code,
                'name' => $curr['name'],
                'rate' => round($curr['rate'] / $baseRate, 8),
                'country' => $curr['country'],
                'flag' => $curr['flag'] ?? null,
                'flag_png' => $curr['flag_png'] ?? null,
                'flag_svg' => $curr['flag_svg'] ?? null,
                'symbol' => $curr['symbol'] ?? null,
            ];
        }

        return response()->json([
            'status' => true,
            'disclaimer' => 'Usage subject to terms: '.url('/terms'),
            'license' => 'Usage subject to license: '.url('/license'),
            'timestamp' => now()->timestamp,
            'base' => $base,
            'rates' => $convertedRates,
        ], 200, [], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
    }

    /**
     * Get Rates by Custom Base Currency
     *
     * Retrieve all active currency rates relative to a specific base currency code passed in the request path.
     */
    public function getAllByDefaultCurrencyPassedInRequest(Request $request, string $code): JsonResponse
    {
        $base = strtoupper($code);
        $currencies = Cache::remember('currencies:all_list', now()->addDays(7), function () {
            return Currency::where('active', true)->get()->keyBy('code')->toArray();
        });

        if (empty($currencies)) {
            Artisan::call('app:sync-currencies');
            Cache::forget('currencies:all_list');
            $currencies = Cache::remember('currencies:all_list', now()->addDays(7), function () {
                return Currency::where('active', true)->get()->keyBy('code')->toArray();
            });
        }

        if (empty($currencies)) {
            return response()->json([
                'status' => false,
                'message' => 'No active currency rates found in system.',
            ], 404);
        }

        if (! isset($currencies[$base])) {
            return response()->json([
                'status' => false,
                'message' => "Currency code '{$base}' is not supported or active.",
            ], 400);
        }

        $baseRate = $currencies[$base]['rate'];
        $convertedRates = [];

        foreach ($currencies as $currCode => $curr) {
            $convertedRates[$currCode] = [
                'code' => $currCode,
                'name' => $curr['name'],
                'rate' => round($curr['rate'] / $baseRate, 8),
                'country' => $curr['country'],
                'flag' => $curr['flag'] ?? null,
                'flag_png' => $curr['flag_png'] ?? null,
                'flag_svg' => $curr['flag_svg'] ?? null,
                'symbol' => $curr['symbol'] ?? null,
            ];
        }

        return response()->json([
            'status' => true,
            'disclaimer' => 'Usage subject to terms: '.url('/terms'),
            'license' => 'Usage subject to license: '.url('/license'),
            'timestamp' => now()->timestamp,
            'base' => $base,
            'rates' => $convertedRates,
        ], 200, [], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
    }

    /**
     * Convert Currency
     *
     * Calculate and convert a specified amount from a source currency to a target currency.
     */
    public function convertCurrency(Request $request, string $from, string $to): JsonResponse
    {
        $from = strtoupper($from);
        $to = strtoupper($to);
        $amount = (float) $request->input('amount', 1.0);

        if ($amount <= 0) {
            return response()->json([
                'status' => false,
                'message' => 'Amount must be greater than zero.',
            ], 400);
        }

        $rates = Cache::remember('currencies:rates', now()->addDays(7), function () {
            return Currency::where('active', true)->pluck('rate', 'code')->toArray();
        });

        if (empty($rates)) {
            return response()->json([
                'status' => false,
                'message' => 'No active currency rates found in system.',
            ], 404);
        }

        if (! isset($rates[$from])) {
            return response()->json([
                'status' => false,
                'message' => "Source currency code '{$from}' is not supported or active.",
            ], 400);
        }

        if (! isset($rates[$to])) {
            return response()->json([
                'status' => false,
                'message' => "Target currency code '{$to}' is not supported or active.",
            ], 400);
        }

        $fromRate = $rates[$from];
        $toRate = $rates[$to];

        $converted = ($amount / $fromRate) * $toRate;
        $rate = $toRate / $fromRate;

        return response()->json([
            'status' => true,
            'from' => $from,
            'to' => $to,
            'amount' => $amount,
            'converted' => round($converted, 4),
            'rate' => round($rate, 6),
            'timestamp' => now()->timestamp,
        ]);
    }

    /**
     * Get Merged Geo and Currency dataset.
     */
    public function allMerged(): JsonResponse
    {
        $merged = Cache::get('geo:all_merged');

        if (empty($merged)) {
            $merged = $this->compileMergedGeo();
        }

        if (empty($merged)) {
            return response()->json([
                'status' => false,
                'message' => 'Geo and currency data not synced or cached. Please run sync command.',
            ], 404);
        }

        return response()->json($merged, 200, [], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
    }

    /**
     * Get all active countries.
     */
    public function countries(): JsonResponse
    {
        if (DB::table('countries')->count() === 0 || DB::table('currencies')->count() === 0) {
            Artisan::call('app:sync-currencies');
            Cache::forget('geo:countries');
        }

        $countries = Cache::remember('geo:countries', now()->addDays(7), function () {
            return DB::table('countries')->get(['id', 'name', 'sortname', 'currency_code'])->map(function ($item) {
                return (array) $item;
            })->toArray();
        });

        if (empty($countries)) {
            return response()->json([
                'status' => false,
                'message' => 'Geo countries data not found.',
            ], 404);
        }

        $currencies = Currency::where('active', true)->get()->keyBy('code');

        $responseCountries = [];
        foreach ($countries as $c) {
            $currencyCode = $c['currency_code'] ?? null;
            $currModel = $currencyCode ? ($currencies[$currencyCode] ?? null) : null;

            $responseCountries[] = [
                'name' => $c['name'],
                'iso_code' => $c['sortname'] ?? null,
                'currency' => $currModel ? [
                    'name' => $currModel->name,
                    'code' => $currModel->code,
                    'symbol' => $currModel->symbol,
                    'flag' => $currModel->flag,
                    'flag_png' => $currModel->flag_png,
                    'flag_svg' => $currModel->flag_svg,
                    'rate' => (float) $currModel->rate,
                    'country' => $currModel->country,
                    'active' => (bool) $currModel->active,
                ] : null,
            ];
        }

        return response()->json([
            'status' => true,
            'countries' => $responseCountries,
        ], 200, [], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
    }

    /**
     * Get states of a country.
     */
    public function states(string $code): JsonResponse
    {
        $code = strtoupper($code);

        if (DB::table('countries')->count() === 0 || DB::table('currencies')->count() === 0) {
            Artisan::call('app:sync-currencies');
            Cache::forget('geo:countries');
            Cache::forget('geo:states_list');
        }

        $countries = Cache::remember('geo:countries', now()->addDays(7), function () {
            return DB::table('countries')->get(['id', 'name', 'sortname', 'currency_code'])->map(function ($item) {
                return (array) $item;
            })->toArray();
        });

        $statesList = Cache::remember('geo:states_list', now()->addDays(7), function () {
            return DB::table('states')->get(['id', 'name', 'country_id'])->map(function ($item) {
                return (array) $item;
            })->toArray();
        });

        $countryId = null;
        $targetCountry = null;
        foreach ($countries as $c) {
            $cca2 = $c['sortname'] ?? null;
            if ($cca2 && (strtoupper($cca2) === $code || strtolower($c['name']) === strtolower($code))) {
                $countryId = $c['id'];
                $targetCountry = $c;
                break;
            }
        }

        if ($countryId === null) {
            return response()->json([
                'status' => false,
                'message' => "Country with code/name '{$code}' not found.",
            ], 404);
        }

        $currencies = Currency::where('active', true)->get()->keyBy('code');
        $currencyCode = $targetCountry['currency_code'] ?? null;
        $currModel = $currencyCode ? ($currencies[$currencyCode] ?? null) : null;

        $countryResponse = [
            'name' => $targetCountry['name'],
            'iso_code' => $targetCountry['sortname'] ?? null,
            'currency' => $currModel ? [
                'name' => $currModel->name,
                'code' => $currModel->code,
                'symbol' => $currModel->symbol,
                'flag' => $currModel->flag,
                'flag_png' => $currModel->flag_png,
                'flag_svg' => $currModel->flag_svg,
                'rate' => (float) $currModel->rate,
                'country' => $currModel->country,
                'active' => (bool) $currModel->active,
            ] : null,
        ];

        $states = [];
        foreach ($statesList as $s) {
            if ($s['country_id'] == $countryId) {
                $states[] = [
                    'name' => $s['name'],
                    'country' => $countryResponse,
                ];
            }
        }

        return response()->json([
            'status' => true,
            'country_code' => $code,
            'states' => $states,
        ], 200, [], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
    }

    /**
     * Get cities of a state.
     */
    public function cities(string $name): JsonResponse
    {
        if (DB::table('countries')->count() === 0 || DB::table('currencies')->count() === 0) {
            Artisan::call('app:sync-currencies');
            Cache::forget('geo:countries');
            Cache::forget('geo:states_list');
            Cache::forget('geo:cities_list');
        }

        $statesList = Cache::remember('geo:states_list', now()->addDays(7), function () {
            return DB::table('states')->get(['id', 'name', 'country_id'])->map(function ($item) {
                return (array) $item;
            })->toArray();
        });

        $stateId = null;
        $targetState = null;
        foreach ($statesList as $s) {
            if (strtolower($s['name']) === strtolower($name)) {
                $stateId = $s['id'];
                $targetState = $s;
                break;
            }
        }

        if ($stateId === null) {
            return response()->json([
                'status' => false,
                'message' => "State '{$name}' not found.",
            ], 404);
        }

        // Find country
        $countryId = $targetState['country_id'];
        $countries = Cache::remember('geo:countries', now()->addDays(7), function () {
            return DB::table('countries')->get(['id', 'name', 'sortname', 'currency_code'])->map(function ($item) {
                return (array) $item;
            })->toArray();
        });

        $targetCountry = null;
        foreach ($countries as $c) {
            if ($c['id'] == $countryId) {
                $targetCountry = $c;
                break;
            }
        }

        $countryResponse = null;
        if ($targetCountry) {
            $currencies = Currency::where('active', true)->get()->keyBy('code');
            $currencyCode = $targetCountry['currency_code'] ?? null;
            $currModel = $currencyCode ? ($currencies[$currencyCode] ?? null) : null;

            $countryResponse = [
                'name' => $targetCountry['name'],
                'iso_code' => $targetCountry['sortname'] ?? null,
                'currency' => $currModel ? [
                    'name' => $currModel->name,
                    'code' => $currModel->code,
                    'symbol' => $currModel->symbol,
                    'flag' => $currModel->flag,
                    'flag_png' => $currModel->flag_png,
                    'flag_svg' => $currModel->flag_svg,
                    'rate' => (float) $currModel->rate,
                    'country' => $currModel->country,
                    'active' => (bool) $currModel->active,
                ] : null,
            ];
        }

        $stateResponse = [
            'name' => $targetState['name'],
            'country' => $countryResponse,
        ];

        $cities = [];
        if (Cache::has('geo:cities_list')) {
            $citiesList = Cache::get('geo:cities_list');
            foreach ($citiesList as $c) {
                if ($c['state_id'] == $stateId) {
                    $cities[] = [
                        'name' => $c['name'],
                        'state' => $stateResponse,
                    ];
                }
            }
        } else {
            $cities = Cache::remember("geo:state_cities_{$stateId}", now()->addDays(7), function () use ($stateId, $stateResponse) {
                return DB::table('cities')
                    ->where('state_id', $stateId)
                    ->get(['name'])
                    ->map(function ($c) use ($stateResponse) {
                        return [
                            'name' => $c->name,
                            'state' => $stateResponse,
                        ];
                    })
                    ->toArray();
            });
        }

        return response()->json([
            'status' => true,
            'state_name' => $name,
            'cities' => $cities,
        ], 200, [], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
    }

    /**
     * Compile Geo cache on the fly from the database.
     */
    protected function compileMergedGeo(): array
    {
        if (DB::table('countries')->count() === 0 || DB::table('currencies')->count() === 0) {
            Artisan::call('app:sync-currencies');
            $merged = Cache::get('geo:all_merged');
            if (! empty($merged)) {
                return $merged;
            }
        }

        $dbCountries = DB::table('countries')->get(['id', 'name', 'sortname', 'currency_code'])->toArray();
        $dbStates = DB::table('states')->get(['id', 'name', 'country_id'])->toArray();
        $dbCities = DB::table('cities')->get(['id', 'name', 'state_id'])->toArray();

        $countryStates = [];
        foreach ($dbStates as $s) {
            $countryStates[$s->country_id][] = [
                'id' => $s->id,
                'name' => $s->name,
            ];
        }

        $stateCities = [];
        foreach ($dbCities as $c) {
            $stateCities[$c->state_id][] = $c->name;
        }

        $mergedGeo = [];
        $activeCurrenciesMapped = Currency::where('active', true)->get()->keyBy('code')->toArray();

        foreach ($dbCountries as $c) {
            $currCode = $c->currency_code ?? null;

            if ($currCode && isset($activeCurrenciesMapped[$currCode])) {
                $curr = $activeCurrenciesMapped[$currCode];
                $countryKey = strtolower($c->name);

                $states = [];
                $sList = $countryStates[$c->id] ?? [];
                foreach ($sList as $stateInfo) {
                    $cities = $stateCities[$stateInfo['id']] ?? [];
                    $states[$stateInfo['name']] = $cities;
                }

                $mergedGeo[$countryKey] = [
                    'currency' => [
                        'name' => $curr['name'],
                        'code' => $curr['code'],
                        'symbol' => $curr['symbol'] ?? null,
                        'flag' => $curr['flag'],
                        'flag_png' => $curr['flag_png'],
                        'flag_svg' => $curr['flag_svg'],
                        'rate' => (float) $curr['rate'],
                        'country' => $curr['country'],
                        'active' => (bool) $curr['active'],
                    ],
                    'states' => $states,
                ];
            }
        }

        $countriesCache = array_map(function ($item) {
            return (array) $item;
        }, $dbCountries);

        $statesCache = array_map(function ($item) {
            return (array) $item;
        }, $dbStates);

        $citiesCache = array_map(function ($item) {
            return (array) $item;
        }, $dbCities);

        Cache::put('geo:countries', $countriesCache, now()->addDays(7));
        Cache::put('geo:states_list', $statesCache, now()->addDays(7));
        Cache::put('geo:cities_list', $citiesCache, now()->addDays(7));
        Cache::put('geo:all_merged', $mergedGeo, now()->addDays(7));

        return $mergedGeo;
    }
}