feat: add more fiat currencies + add many crypto currencies

This commit is contained in:
2026-04-13 00:00:18 +03:00
parent 3a03aad9de
commit 15436c285e
10 changed files with 64539 additions and 20 deletions

View File

@@ -1,7 +1,4 @@
{
"plugins": [
"prettier-plugin-vue"
],
"printWidth": 100,
"semi": false,
"singleQuote": true,

View File

@@ -123,6 +123,47 @@ class ApiController extends OCSController {
);
}
/**
* Get crypto currencies with pagination and search
*
* @param string|null $search Search query to filter by code or name
* @param int<1, 200> $limit Max results per page
* @param int $offset Offset for pagination
* @return DataResponse<Http::STATUS_OK, array{
* currencies: list<array{code: string, symbol: string, name: string}>,
* total: int
* }, array{}>
*
* 200: Data returned
*/
#[NoAdminRequired]
#[ApiRoute(verb: 'GET', url: '/api/crypto-currencies')]
public function getCryptoCurrencies(
?string $search = null,
int $limit = 50,
int $offset = 0,
): DataResponse {
$all = $this->service->getCryptoSymbols();
if ($search !== null && trim($search) !== '') {
$q = mb_strtolower(trim($search), 'UTF-8');
$all = array_filter($all, function ($sym) use ($q) {
return str_contains(mb_strtolower((string)($sym['code'] ?? ''), 'UTF-8'), $q)
|| str_contains(mb_strtolower((string)($sym['name'] ?? ''), 'UTF-8'), $q);
});
}
$total = count($all);
$page = array_slice(array_values($all), $offset, max(1, min($limit, 200)));
$currencies = array_map(
fn ($sym): array => ['name' => $sym['name'], 'code' => $sym['code'], 'symbol' => $sym['symbol']],
$page
);
return new DataResponse(['currencies' => $currencies, 'total' => $total]);
}
/**
* Run cron immediately
*
@@ -224,7 +265,7 @@ class ApiController extends OCSController {
* @param string|null $currency Quoted currency code to filter (e.g. "eur")
* @param string|null $from ISO-8601 datetime (inclusive)
* @param string|null $to ISO-8601 datetime (inclusive)
* @param int|null $limit Max rows to return (optional)
* @param int<1, 1000>|null $limit Max rows to return (optional)
* @param int|null $offset Offset for pagination (optional)
*
* @return DataResponse<Http::STATUS_OK, array{

View File

@@ -28,10 +28,17 @@ use Psr\Log\LoggerInterface;
class FetchCurrenciesService {
private static $EXCHANGE_URL = 'https://cdn.jsdelivr.net/npm/@fawazahmed0/currency-api@latest/v1/currencies/{base}.json';
private static $SYMBOLS_FILE = __DIR__ . '/symbols.json';
private static $CRYPTO_SYMBOLS_FILE = __DIR__ . '/symbols-crypto.json';
/** @var array<string, mixed> */
/** @var array<string, mixed> Fiat currencies (displayed in UI) */
public array $symbols = [];
/** @var array<string, mixed>|null Crypto currencies (lazy-loaded) */
private ?array $cryptoSymbols = null;
/** @var array<string, mixed>|null All currencies including crypto (lazy-loaded) */
private ?array $allSymbols = null;
/** @var array<string, mixed> */
private array $symbolPreference = [];
@@ -489,7 +496,7 @@ class FetchCurrenciesService {
$lower = mb_strtolower($original, 'UTF-8');
// Prefer explicit 3-letter code tokens (e.g., "US Dollar (USD)")
foreach ($this->symbols as $code => $currency) {
foreach ($this->getAllSymbols() as $code => $currency) {
$id = mb_strtolower((string)$code, 'UTF-8');
if (preg_match('/\b' . preg_quote($id, '/') . '\b/u', $lower)) {
return $id;
@@ -501,7 +508,7 @@ class FetchCurrenciesService {
// Collect all codes hit by symbols, then resolve with preference.
$hitsBySymbol = [];
foreach ($this->symbols as $code => $currency) {
foreach ($this->getAllSymbols() as $code => $currency) {
if (empty($currency['symbol'])) {
continue;
}
@@ -543,10 +550,35 @@ class FetchCurrenciesService {
return null;
}
/** Load symbols from the symbols.json file */
/**
* Get crypto symbols (lazy-loaded)
* @return array<string, mixed>
*/
public function getCryptoSymbols(): array {
if ($this->cryptoSymbols === null) {
$cryptoFile = FetchCurrenciesService::$CRYPTO_SYMBOLS_FILE;
$this->cryptoSymbols = file_exists($cryptoFile)
? (json_decode(file_get_contents($cryptoFile), true) ?? [])
: [];
}
return $this->cryptoSymbols;
}
/**
* Get all symbols - fiat + crypto (lazy-loaded)
* @return array<string, mixed>
*/
public function getAllSymbols(): array {
if ($this->allSymbols === null) {
$this->allSymbols = array_merge($this->symbols, $this->getCryptoSymbols());
}
return $this->allSymbols;
}
/** Load fiat symbols from JSON file */
private function loadSymbols(): void {
$this->symbols = json_decode(file_get_contents(FetchCurrenciesService::$SYMBOLS_FILE), true);
$this->logger->debug('Loaded symbols: ' . json_encode($this->symbols));
$this->symbols = json_decode(file_get_contents(FetchCurrenciesService::$SYMBOLS_FILE), true) ?? [];
$this->logger->debug('Loaded ' . count($this->symbols) . ' fiat symbols');
}
/**

File diff suppressed because it is too large Load Diff

View File

@@ -35,6 +35,24 @@
"code": "AMD",
"name_plural": "Armenian drams"
},
"ANG": {
"symbol": "ANG",
"name": "Netherlands Antillean Guilder",
"symbol_native": "ƒ",
"decimal_digits": 2,
"rounding": 0,
"code": "ANG",
"name_plural": "Netherlands Antillean guilders"
},
"AOA": {
"symbol": "AOA",
"name": "Angolan Kwanza",
"symbol_native": "Kz",
"decimal_digits": 2,
"rounding": 0,
"code": "AOA",
"name_plural": "Angolan kwanzas"
},
"ARS": {
"symbol": "AR$",
"name": "Argentine Peso",
@@ -53,6 +71,15 @@
"code": "AUD",
"name_plural": "Australian dollars"
},
"AWG": {
"symbol": "AWG",
"name": "Aruban Florin",
"symbol_native": "ƒ",
"decimal_digits": 2,
"rounding": 0,
"code": "AWG",
"name_plural": "Aruban florin"
},
"AZN": {
"symbol": "man.",
"name": "Azerbaijani Manat",
@@ -71,6 +98,15 @@
"code": "BAM",
"name_plural": "Bosnia-Herzegovina convertible marks"
},
"BBD": {
"symbol": "BBD",
"name": "Barbadian Dollar",
"symbol_native": "$",
"decimal_digits": 2,
"rounding": 0,
"code": "BBD",
"name_plural": "Barbadian dollars"
},
"BDT": {
"symbol": "Tk",
"name": "Bangladeshi Taka",
@@ -107,6 +143,15 @@
"code": "BIF",
"name_plural": "Burundian francs"
},
"BMD": {
"symbol": "BMD",
"name": "Bermudian Dollar",
"symbol_native": "$",
"decimal_digits": 2,
"rounding": 0,
"code": "BMD",
"name_plural": "Bermudian dollars"
},
"BND": {
"symbol": "BN$",
"name": "Brunei Dollar",
@@ -134,6 +179,24 @@
"code": "BRL",
"name_plural": "Brazilian reals"
},
"BSD": {
"symbol": "BSD",
"name": "Bahamian Dollar",
"symbol_native": "$",
"decimal_digits": 2,
"rounding": 0,
"code": "BSD",
"name_plural": "Bahamian dollars"
},
"BTN": {
"symbol": "BTN",
"name": "Bhutanese Ngultrum",
"symbol_native": "Nu.",
"decimal_digits": 2,
"rounding": 0,
"code": "BTN",
"name_plural": "Bhutanese ngultrums"
},
"BWP": {
"symbol": "BWP",
"name": "Botswanan Pula",
@@ -224,6 +287,15 @@
"code": "CRC",
"name_plural": "Costa Rican colóns"
},
"CUC": {
"symbol": "CUC",
"name": "Cuban Convertible Peso",
"symbol_native": "$",
"decimal_digits": 2,
"rounding": 0,
"code": "CUC",
"name_plural": "Cuban Convertible Pesos"
},
"CUP": {
"symbol": "MN$",
"name": "Cuban peso",
@@ -296,6 +368,15 @@
"code": "EGP",
"name_plural": "Egyptian pounds"
},
"ERN": {
"symbol": "ERN",
"name": "Eritrean Nakfa",
"symbol_native": "Nfk",
"decimal_digits": 2,
"rounding": 0,
"code": "ERN",
"name_plural": "Eritrean nakfas"
},
"ETB": {
"symbol": "Br",
"name": "Ethiopian Birr",
@@ -314,6 +395,24 @@
"code": "EUR",
"name_plural": "Euros"
},
"FJD": {
"symbol": "FJD",
"name": "Fijian Dollar",
"symbol_native": "$",
"decimal_digits": 2,
"rounding": 0,
"code": "FJD",
"name_plural": "Fijian dollars"
},
"FKP": {
"symbol": "FKP",
"name": "Falkland Islands Pound",
"symbol_native": "£",
"decimal_digits": 2,
"rounding": 0,
"code": "FKP",
"name_plural": "Falkland Islands pounds"
},
"GBP": {
"symbol": "£",
"name": "British Pound Sterling",
@@ -332,6 +431,15 @@
"code": "GEL",
"name_plural": "Georgian laris"
},
"GGP": {
"symbol": "GGP",
"name": "GamingGoProject",
"symbol_native": "GGP",
"decimal_digits": 8,
"rounding": 0,
"code": "GGP",
"name_plural": "GamingGoProject"
},
"GHS": {
"symbol": "GH₵",
"name": "Ghanaian Cedi",
@@ -341,6 +449,24 @@
"code": "GHS",
"name_plural": "Ghanaian cedis"
},
"GIP": {
"symbol": "GIP",
"name": "Gibraltar Pound",
"symbol_native": "£",
"decimal_digits": 2,
"rounding": 0,
"code": "GIP",
"name_plural": "Gibraltar pounds"
},
"GMD": {
"symbol": "GMD",
"name": "Gambian Dalasi",
"symbol_native": "D",
"decimal_digits": 2,
"rounding": 0,
"code": "GMD",
"name_plural": "Gambian dalasis"
},
"GNF": {
"symbol": "FG",
"name": "Guinean Franc",
@@ -359,6 +485,15 @@
"code": "GTQ",
"name_plural": "Guatemalan quetzals"
},
"GYD": {
"symbol": "GYD",
"name": "Guyanese Dollar",
"symbol_native": "$",
"decimal_digits": 2,
"rounding": 0,
"code": "GYD",
"name_plural": "Guyanese dollars"
},
"HKD": {
"symbol": "HK$",
"name": "Hong Kong Dollar",
@@ -377,6 +512,24 @@
"code": "HNL",
"name_plural": "Honduran lempiras"
},
"HRK": {
"symbol": "HRK",
"name": "Croatian Kuna",
"symbol_native": "HRK",
"decimal_digits": 8,
"rounding": 0,
"code": "HRK",
"name_plural": "Croatian Kuna"
},
"HTG": {
"symbol": "HTG",
"name": "Haitian Gourde",
"symbol_native": "G",
"decimal_digits": 2,
"rounding": 0,
"code": "HTG",
"name_plural": "Haitian gourdes"
},
"HUF": {
"symbol": "Ft",
"name": "Hungarian Forint",
@@ -404,6 +557,15 @@
"code": "ILS",
"name_plural": "Israeli new sheqels"
},
"IMP": {
"symbol": "IMP",
"name": "Ether Kingdoms Token",
"symbol_native": "IMP",
"decimal_digits": 8,
"rounding": 0,
"code": "IMP",
"name_plural": "Ether Kingdoms Token"
},
"INR": {
"symbol": "₹",
"name": "Indian Rupee",
@@ -440,6 +602,15 @@
"code": "ISK",
"name_plural": "Icelandic krónur"
},
"JEP": {
"symbol": "JEP",
"name": "Jersey Pound",
"symbol_native": "JEP",
"decimal_digits": 8,
"rounding": 0,
"code": "JEP",
"name_plural": "Jersey Pound"
},
"JMD": {
"symbol": "J$",
"name": "Jamaican Dollar",
@@ -476,6 +647,15 @@
"code": "KES",
"name_plural": "Kenyan shillings"
},
"KGS": {
"symbol": "KGS",
"name": "Kyrgyzstani Som",
"symbol_native": "с",
"decimal_digits": 2,
"rounding": 0,
"code": "KGS",
"name_plural": "Kyrgyzstani soms"
},
"KHR": {
"symbol": "KHR",
"name": "Cambodian Riel",
@@ -494,6 +674,15 @@
"code": "KMF",
"name_plural": "Comorian francs"
},
"KPW": {
"symbol": "KPW",
"name": "North Korean Won",
"symbol_native": "₩",
"decimal_digits": 2,
"rounding": 0,
"code": "KPW",
"name_plural": "North Korean won"
},
"KRW": {
"symbol": "₩",
"name": "South Korean Won",
@@ -512,6 +701,15 @@
"code": "KWD",
"name_plural": "Kuwaiti dinars"
},
"KYD": {
"symbol": "KYD",
"name": "Cayman Islands Dollar",
"symbol_native": "$",
"decimal_digits": 2,
"rounding": 0,
"code": "KYD",
"name_plural": "Cayman Islands dollars"
},
"KZT": {
"symbol": "KZT",
"name": "Kazakhstani Tenge",
@@ -521,6 +719,15 @@
"code": "KZT",
"name_plural": "Kazakhstani tenges"
},
"LAK": {
"symbol": "LAK",
"name": "Lao Kip",
"symbol_native": "₭",
"decimal_digits": 2,
"rounding": 0,
"code": "LAK",
"name_plural": "Lao kips"
},
"LBP": {
"symbol": "L.L.",
"name": "Lebanese Pound",
@@ -539,6 +746,24 @@
"code": "LKR",
"name_plural": "Sri Lankan rupees"
},
"LRD": {
"symbol": "LRD",
"name": "Liberian Dollar",
"symbol_native": "$",
"decimal_digits": 2,
"rounding": 0,
"code": "LRD",
"name_plural": "Liberian dollars"
},
"LSL": {
"symbol": "LSL",
"name": "Lesotho Loti",
"symbol_native": "L",
"decimal_digits": 2,
"rounding": 0,
"code": "LSL",
"name_plural": "Lesotho maloti"
},
"LYD": {
"symbol": "LD",
"name": "Libyan Dinar",
@@ -593,6 +818,15 @@
"code": "MMK",
"name_plural": "Myanma kyats"
},
"MNT": {
"symbol": "MNT",
"name": "Mongolian Tugrik",
"symbol_native": "₮",
"decimal_digits": 2,
"rounding": 0,
"code": "MNT",
"name_plural": "Mongolian tugriks"
},
"MOP": {
"symbol": "MOP$",
"name": "Macanese Pataca",
@@ -602,6 +836,24 @@
"code": "MOP",
"name_plural": "Macanese patacas"
},
"MRO": {
"symbol": "MRO",
"name": "Mauritanian Ouguiya",
"symbol_native": "MRO",
"decimal_digits": 8,
"rounding": 0,
"code": "MRO",
"name_plural": "Mauritanian Ouguiya"
},
"MRU": {
"symbol": "MRU",
"name": "Mauritanian Ouguiya",
"symbol_native": "UM",
"decimal_digits": 2,
"rounding": 0,
"code": "MRU",
"name_plural": "Mauritanian ouguiyas"
},
"MUR": {
"symbol": "MURs",
"name": "Mauritian Rupee",
@@ -611,6 +863,24 @@
"code": "MUR",
"name_plural": "Mauritian rupees"
},
"MVR": {
"symbol": "MVR",
"name": "Maldivian Rufiyaa",
"symbol_native": ".ރ",
"decimal_digits": 2,
"rounding": 0,
"code": "MVR",
"name_plural": "Maldivian rufiyaas"
},
"MWK": {
"symbol": "MWK",
"name": "Malawian Kwacha",
"symbol_native": "MK",
"decimal_digits": 2,
"rounding": 0,
"code": "MWK",
"name_plural": "Malawian kwachas"
},
"MXN": {
"symbol": "MX$",
"name": "Mexican Peso",
@@ -719,6 +989,15 @@
"code": "PEN",
"name_plural": "Peruvian nuevos soles"
},
"PGK": {
"symbol": "PGK",
"name": "Papua New Guinean Kina",
"symbol_native": "K",
"decimal_digits": 2,
"rounding": 0,
"code": "PGK",
"name_plural": "Papua New Guinean kina"
},
"PHP": {
"symbol": "₱",
"name": "Philippine Peso",
@@ -809,6 +1088,24 @@
"code": "SAR",
"name_plural": "Saudi riyals"
},
"SBD": {
"symbol": "SBD",
"name": "Solomon Islands Dollar",
"symbol_native": "$",
"decimal_digits": 2,
"rounding": 0,
"code": "SBD",
"name_plural": "Solomon Islands dollars"
},
"SCR": {
"symbol": "SCR",
"name": "Seychellois Rupee",
"symbol_native": "₨",
"decimal_digits": 2,
"rounding": 0,
"code": "SCR",
"name_plural": "Seychellois rupees"
},
"SDG": {
"symbol": "SDG",
"name": "Sudanese Pound",
@@ -836,6 +1133,33 @@
"code": "SGD",
"name_plural": "Singapore dollars"
},
"SHP": {
"symbol": "SHP",
"name": "Saint Helena Pound",
"symbol_native": "£",
"decimal_digits": 2,
"rounding": 0,
"code": "SHP",
"name_plural": "Saint Helena pounds"
},
"SLE": {
"symbol": "SLE",
"name": "Sierra Leonean Leone",
"symbol_native": "SLE",
"decimal_digits": 8,
"rounding": 0,
"code": "SLE",
"name_plural": "Sierra Leonean Leone"
},
"SLL": {
"symbol": "SLL",
"name": "Sierra Leonean Leone (Old)",
"symbol_native": "Le",
"decimal_digits": 2,
"rounding": 0,
"code": "SLL",
"name_plural": "Sierra Leonean leones"
},
"SOS": {
"symbol": "Ssh",
"name": "Somali Shilling",
@@ -845,6 +1169,78 @@
"code": "SOS",
"name_plural": "Somali shillings"
},
"SPL": {
"symbol": "SPL",
"name": "Seborgan Luigino",
"symbol_native": "SPL",
"decimal_digits": 8,
"rounding": 0,
"code": "SPL",
"name_plural": "Seborgan Luigino"
},
"SRD": {
"symbol": "SRD",
"name": "Surinamese Dollar",
"symbol_native": "$",
"decimal_digits": 2,
"rounding": 0,
"code": "SRD",
"name_plural": "Surinamese dollars"
},
"SSP": {
"symbol": "SSP",
"name": "South Sudanese Pound",
"symbol_native": "£",
"decimal_digits": 2,
"rounding": 0,
"code": "SSP",
"name_plural": "South Sudanese pounds"
},
"STD": {
"symbol": "STD",
"name": "Sao Tomean Dobra",
"symbol_native": "STD",
"decimal_digits": 8,
"rounding": 0,
"code": "STD",
"name_plural": "Sao Tomean Dobra"
},
"STN": {
"symbol": "STN",
"name": "São Tomé and Príncipe Dobra",
"symbol_native": "Db",
"decimal_digits": 2,
"rounding": 0,
"code": "STN",
"name_plural": "São Tomé and Príncipe dobras"
},
"SVC": {
"symbol": "SVC",
"name": "Salvadoran Colon",
"symbol_native": "SVC",
"decimal_digits": 8,
"rounding": 0,
"code": "SVC",
"name_plural": "Salvadoran Colon"
},
"SYP": {
"symbol": "SYP",
"name": "Syrian Pound",
"symbol_native": "ل.س",
"decimal_digits": 2,
"rounding": 0,
"code": "SYP",
"name_plural": "Syrian pounds"
},
"SZL": {
"symbol": "SZL",
"name": "Swazi Lilangeni",
"symbol_native": "E",
"decimal_digits": 2,
"rounding": 0,
"code": "SZL",
"name_plural": "Swazi emalangeni"
},
"THB": {
"symbol": "฿",
"name": "Thai Baht",
@@ -854,6 +1250,24 @@
"code": "THB",
"name_plural": "Thai baht"
},
"TJS": {
"symbol": "TJS",
"name": "Tajikistani Somoni",
"symbol_native": "SM",
"decimal_digits": 2,
"rounding": 0,
"code": "TJS",
"name_plural": "Tajikistani somonis"
},
"TMT": {
"symbol": "TMT",
"name": "Turkmenistani Manat",
"symbol_native": "m",
"decimal_digits": 2,
"rounding": 0,
"code": "TMT",
"name_plural": "Turkmenistani manats"
},
"TND": {
"symbol": "DT",
"name": "Tunisian Dinar",
@@ -890,6 +1304,15 @@
"code": "TTD",
"name_plural": "Trinidad and Tobago dollars"
},
"TVD": {
"symbol": "TVD",
"name": "Tuvaluan Dollar",
"symbol_native": "TVD",
"decimal_digits": 8,
"rounding": 0,
"code": "TVD",
"name_plural": "Tuvaluan Dollar"
},
"TWD": {
"symbol": "NT$",
"name": "New Taiwan Dollar",
@@ -953,6 +1376,15 @@
"code": "UZS",
"name_plural": "Uzbekistan som"
},
"VES": {
"symbol": "VES",
"name": "Venezuelan Bolívar",
"symbol_native": "Bs.",
"decimal_digits": 2,
"rounding": 0,
"code": "VES",
"name_plural": "Venezuelan bolívars"
},
"VND": {
"symbol": "₫",
"name": "Vietnamese Dong",
@@ -962,6 +1394,24 @@
"code": "VND",
"name_plural": "Vietnamese dong"
},
"VUV": {
"symbol": "VUV",
"name": "Vanuatu Vatu",
"symbol_native": "VT",
"decimal_digits": 0,
"rounding": 0,
"code": "VUV",
"name_plural": "Vanuatu vatus"
},
"WST": {
"symbol": "WST",
"name": "Samoan Tala",
"symbol_native": "T",
"decimal_digits": 2,
"rounding": 0,
"code": "WST",
"name_plural": "Samoan tala"
},
"XAF": {
"symbol": "FCFA",
"name": "CFA Franc BEAC",
@@ -980,6 +1430,15 @@
"code": "XCD",
"name_plural": "Eastern Caribbean dollars"
},
"XDR": {
"symbol": "XDR",
"name": "IMF Special Drawing Rights",
"symbol_native": "XDR",
"decimal_digits": 8,
"rounding": 0,
"code": "XDR",
"name_plural": "IMF Special Drawing Rights"
},
"XOF": {
"symbol": "CFA",
"name": "CFA Franc BCEAO",
@@ -989,6 +1448,15 @@
"code": "XOF",
"name_plural": "CFA francs BCEAO"
},
"XPF": {
"symbol": "XPF",
"name": "CFP Franc",
"symbol_native": "₣",
"decimal_digits": 0,
"rounding": 0,
"code": "XPF",
"name_plural": "CFP francs"
},
"YER": {
"symbol": "YR",
"name": "Yemeni Rial",
@@ -1006,5 +1474,32 @@
"rounding": 0,
"code": "ZAR",
"name_plural": "South African rand"
},
"ZMW": {
"symbol": "ZMW",
"name": "Zambian Kwacha",
"symbol_native": "ZK",
"decimal_digits": 2,
"rounding": 0,
"code": "ZMW",
"name_plural": "Zambian kwachas"
},
"ZWD": {
"symbol": "ZWD",
"name": "Zimbabwean Dollar",
"symbol_native": "ZWD",
"decimal_digits": 8,
"rounding": 0,
"code": "ZWD",
"name_plural": "Zimbabwean Dollar"
},
"ZWL": {
"symbol": "ZWL",
"name": "Zimbabwean Dollar",
"symbol_native": "ZWL",
"decimal_digits": 8,
"rounding": 0,
"code": "ZWL",
"name_plural": "Zimbabwean Dollar"
}
}

View File

@@ -1409,6 +1409,159 @@
}
}
},
"/ocs/v2.php/apps/autocurrency/api/crypto-currencies": {
"get": {
"operationId": "api-get-crypto-currencies",
"summary": "Get crypto currencies with pagination and search",
"tags": [
"api"
],
"security": [
{
"bearer_auth": []
},
{
"basic_auth": []
}
],
"parameters": [
{
"name": "search",
"in": "query",
"description": "Search query to filter by code or name",
"schema": {
"type": "string",
"nullable": true,
"default": null
}
},
{
"name": "limit",
"in": "query",
"description": "Max results per page",
"schema": {
"type": "integer",
"format": "int64",
"default": 50,
"minimum": 1,
"maximum": 200
}
},
{
"name": "offset",
"in": "query",
"description": "Offset for pagination",
"schema": {
"type": "integer",
"format": "int64",
"default": 0
}
},
{
"name": "OCS-APIRequest",
"in": "header",
"description": "Required to be true for the API request to pass",
"required": true,
"schema": {
"type": "boolean",
"default": true
}
}
],
"responses": {
"200": {
"description": "Data returned",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"ocs"
],
"properties": {
"ocs": {
"type": "object",
"required": [
"meta",
"data"
],
"properties": {
"meta": {
"$ref": "#/components/schemas/OCSMeta"
},
"data": {
"type": "object",
"required": [
"currencies",
"total"
],
"properties": {
"currencies": {
"type": "array",
"items": {
"type": "object",
"required": [
"code",
"symbol",
"name"
],
"properties": {
"code": {
"type": "string"
},
"symbol": {
"type": "string"
},
"name": {
"type": "string"
}
}
}
},
"total": {
"type": "integer",
"format": "int64"
}
}
}
}
}
}
}
}
}
},
"401": {
"description": "Current user is not logged in",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"ocs"
],
"properties": {
"ocs": {
"type": "object",
"required": [
"meta",
"data"
],
"properties": {
"meta": {
"$ref": "#/components/schemas/OCSMeta"
},
"data": {}
}
}
}
}
}
}
}
}
}
},
"/ocs/v2.php/apps/autocurrency/api/projects": {
"get": {
"operationId": "api-get-projects",
@@ -1589,7 +1742,9 @@
"type": "integer",
"format": "int64",
"nullable": true,
"default": null
"default": null,
"minimum": 1,
"maximum": 1000
}
},
{

View File

@@ -163,6 +163,159 @@
}
}
},
"/ocs/v2.php/apps/autocurrency/api/crypto-currencies": {
"get": {
"operationId": "api-get-crypto-currencies",
"summary": "Get crypto currencies with pagination and search",
"tags": [
"api"
],
"security": [
{
"bearer_auth": []
},
{
"basic_auth": []
}
],
"parameters": [
{
"name": "search",
"in": "query",
"description": "Search query to filter by code or name",
"schema": {
"type": "string",
"nullable": true,
"default": null
}
},
{
"name": "limit",
"in": "query",
"description": "Max results per page",
"schema": {
"type": "integer",
"format": "int64",
"default": 50,
"minimum": 1,
"maximum": 200
}
},
{
"name": "offset",
"in": "query",
"description": "Offset for pagination",
"schema": {
"type": "integer",
"format": "int64",
"default": 0
}
},
{
"name": "OCS-APIRequest",
"in": "header",
"description": "Required to be true for the API request to pass",
"required": true,
"schema": {
"type": "boolean",
"default": true
}
}
],
"responses": {
"200": {
"description": "Data returned",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"ocs"
],
"properties": {
"ocs": {
"type": "object",
"required": [
"meta",
"data"
],
"properties": {
"meta": {
"$ref": "#/components/schemas/OCSMeta"
},
"data": {
"type": "object",
"required": [
"currencies",
"total"
],
"properties": {
"currencies": {
"type": "array",
"items": {
"type": "object",
"required": [
"code",
"symbol",
"name"
],
"properties": {
"code": {
"type": "string"
},
"symbol": {
"type": "string"
},
"name": {
"type": "string"
}
}
}
},
"total": {
"type": "integer",
"format": "int64"
}
}
}
}
}
}
}
}
}
},
"401": {
"description": "Current user is not logged in",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"ocs"
],
"properties": {
"ocs": {
"type": "object",
"required": [
"meta",
"data"
],
"properties": {
"meta": {
"$ref": "#/components/schemas/OCSMeta"
},
"data": {}
}
}
}
}
}
}
}
}
}
},
"/ocs/v2.php/apps/autocurrency/api/projects": {
"get": {
"operationId": "api-get-projects",
@@ -343,7 +496,9 @@
"type": "integer",
"format": "int64",
"nullable": true,
"default": null
"default": null,
"minimum": 1,
"maximum": 1000
}
},
{

View File

@@ -21,9 +21,11 @@
<li> <code>US Dollar</code></li>
<li> <code>United States Dollar</code></li>
</ul>
</NcSettingsSection>
<NcSettingsSection :name="strings.supportedCurrenciesTitle">
<div class="currency-list">
<p>{{ strings.supportedCurrencies }}</p>
<p>{{ strings.supportedFiatCurrencies(supportedCurrencies.length) }}</p>
<div style="max-width: 300px">
<NcTextField
@@ -53,6 +55,41 @@
</tbody>
</table>
</div>
<div class="currency-list mt">
<p>{{ strings.supportedCryptoCurrencies(cryptoTotalAll) }}</p>
<div style="max-width: 300px">
<NcTextField
v-model="cryptoSearch"
:label="strings.currencySearchLabel"
trailing-button-icon="close"
:placeholder="strings.cryptoSearchPlaceholder"
:show-trailing-button="cryptoSearch !== ''"
@trailing-button-click="clearCryptoSearch"
/>
</div>
<table>
<thead>
<tr>
<th>{{ strings.tableSymbol }}</th>
<th>{{ strings.tableCode }}</th>
<th>{{ strings.tableName }}</th>
</tr>
</thead>
<tbody ref="cryptoTbody" @scroll="onCryptoScroll">
<tr v-for="currency in cryptoCurrencies" :key="currency.code">
<td>{{ currency.symbol }}</td>
<td>{{ currency.code }}</td>
<td>{{ currency.name }}</td>
</tr>
<tr v-if="cryptoLoading">
<td colspan="3" style="text-align: center">{{ strings.loading }}</td>
</tr>
</tbody>
</table>
</div>
</NcSettingsSection>
<NcSettingsSection :name="strings.historyHeader">
@@ -154,6 +191,14 @@ export default {
loading: true as boolean,
supportedCurrencies: [] as SupportedCurrency[],
currencySearch: '' as string,
cryptoCurrencies: [] as SupportedCurrency[],
cryptoSearch: '' as string,
cryptoSearchDebounce: null as ReturnType<typeof setTimeout> | null,
cryptoTotalAll: 0 as number,
cryptoTotal: 0 as number,
cryptoOffset: 0 as number,
cryptoLoading: false as boolean,
cryptoHasMore: true as boolean,
projectsLoading: true as boolean,
projects: [] as Project[],
selectedProject: null as Project | null,
@@ -204,7 +249,13 @@ export default {
{ escape: false },
),
exampleHeader: t(APP_ID, 'Example names:'),
supportedCurrencies: t(APP_ID, 'Supported currencies:'),
supportedCurrenciesTitle: t(APP_ID, 'Supported currencies'),
supportedFiatCurrencies: (count: number) =>
t(APP_ID, 'Supported fiat currencies ({count}):', { count: count.toLocaleString() }),
supportedCryptoCurrencies: (count: number) =>
t(APP_ID, 'Supported cryptocurrencies ({count}):', { count: count.toLocaleString() }),
loading: t(APP_ID, 'Loading…'),
cryptoSearchPlaceholder: t(APP_ID, 'e.g. BTC, Bitcoin'),
currencySearchLabel: t(APP_ID, 'Search'),
currencySearchPlaceholder: t(APP_ID, 'e.g. $, USD, US Dollar'),
tableSymbol: t(APP_ID, 'Symbol'),
@@ -223,6 +274,7 @@ export default {
},
async created() {
await this.fetchSettings()
this.fetchCryptoCurrencies()
await this.fetchProjects()
},
watch: {
@@ -246,6 +298,15 @@ export default {
showReversed() {
this.$nextTick(() => this.renderChart())
},
cryptoSearch() {
if (this.cryptoSearchDebounce) clearTimeout(this.cryptoSearchDebounce)
this.cryptoSearchDebounce = setTimeout(() => {
this.cryptoCurrencies = []
this.cryptoOffset = 0
this.cryptoHasMore = true
this.fetchCryptoCurrencies()
}, 300)
},
},
methods: {
formatDate(value: Date | string | null | undefined): string {
@@ -254,8 +315,8 @@ export default {
value instanceof Date
? value
: typeof value === 'string'
? parseDate(value)
: new Date(value as any)
? parseDate(value)
: new Date(value as any)
if (!isValid(d)) {
console.warn('Invalid date received:', value)
@@ -288,6 +349,45 @@ export default {
this.currencySearch = ''
},
clearCryptoSearch() {
this.cryptoSearch = ''
},
async fetchCryptoCurrencies() {
if (this.cryptoLoading || !this.cryptoHasMore) return
try {
this.cryptoLoading = true
const params: Record<string, string | number> = {
limit: 50,
offset: this.cryptoOffset,
}
if (this.cryptoSearch.trim()) {
params.search = this.cryptoSearch.trim()
}
const resp = await ocs.get('/crypto-currencies', { params })
const data = resp.data ?? {}
const items: SupportedCurrency[] = data.currencies ?? []
this.cryptoTotal = data.total ?? 0
if (this.cryptoTotalAll === 0) {
this.cryptoTotalAll = this.cryptoTotal
}
this.cryptoCurrencies = [...this.cryptoCurrencies, ...items]
this.cryptoOffset += items.length
this.cryptoHasMore = this.cryptoOffset < this.cryptoTotal
} catch (e) {
console.error('Failed to fetch crypto currencies', e)
} finally {
this.cryptoLoading = false
}
},
onCryptoScroll(e: Event) {
const el = e.target as HTMLElement
if (el.scrollTop + el.clientHeight >= el.scrollHeight - 40) {
this.fetchCryptoCurrencies()
}
},
async fetchProjects() {
try {
this.projectsLoading = true
@@ -571,7 +671,10 @@ export default {
display: flex;
flex-direction: column;
gap: 8px;
margin-top: 2em;
&.mt {
margin-top: 2em;
}
}
.history-block {
@@ -586,7 +689,7 @@ export default {
@media (max-width: 1280px) {
grid-template-columns: 1fr 1fr;
>* {
> * {
width: 100%;
}
}

View File

@@ -58,6 +58,10 @@ final class FetchCurrenciesServiceTest extends TestCase {
$propSymbols = $ref->getProperty('symbols');
$propSymbols->setAccessible(true);
$propSymbols->setValue($this->resolver, $symbols);
$propAllSymbols = $ref->getProperty('allSymbols');
$propAllSymbols->setAccessible(true);
$propAllSymbols->setValue($this->resolver, $symbols);
}
/**

View File

@@ -31,8 +31,12 @@ async function main() {
console.log(`Fetched ${Object.keys(rates.usd).length} currencies`)
// eslint-disable-next-line @typescript-eslint/no-require-imports
const symbols = require('../lib/Service/symbols.json')
const finalJson = findIntersections(symbols, rates)
const fiatSymbols = require('../lib/Service/symbols.json')
// eslint-disable-next-line @typescript-eslint/no-require-imports
const cryptoSymbols = require('../lib/Service/symbols-crypto.json')
const allSymbols = { ...fiatSymbols, ...cryptoSymbols }
const finalJson = findIntersections(allSymbols, rates)
console.log(JSON.stringify(finalJson, null, 2))
}